}
*/
- const checkScopes = (req, channelName) => new Promise((resolve, reject) => {
- log.silly(req.requestId, `Checking OAuth scopes for ${channelName}`);
+ const checkScopes = (req, logger, channelName) => new Promise((resolve, reject) => {
+ logger.debug(`Checking OAuth scopes for ${channelName}`);
// When accessing public channels, no scopes are needed
- if (PUBLIC_CHANNELS.includes(channelName)) {
+ if (channelName && PUBLIC_CHANNELS.includes(channelName)) {
resolve();
return;
}
@@ -564,6 +569,7 @@ const startServer = async () => {
}
const err = new Error('Access token does not cover required scopes');
+ // @ts-ignore
err.status = 401;
reject(err);
@@ -577,38 +583,40 @@ const startServer = async () => {
/**
* @param {any} req
* @param {SystemMessageHandlers} eventHandlers
- * @returns {function(object): void}
+ * @returns {SubscriptionListener}
*/
const createSystemMessageListener = (req, eventHandlers) => {
return message => {
+ if (!message?.event) {
+ return;
+ }
+
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') {
- 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();
} 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;
}
};
};
/**
- * @param {any} req
- * @param {any} res
+ * @param {http.IncomingMessage & ResolvedAccount} req
+ * @param {http.OutgoingMessage} res
*/
const subscribeHttpToSystemChannel = (req, res) => {
const accessTokenChannelId = `timeline:access_token:${req.accessTokenId}`;
const systemChannelId = `timeline:system:${req.accountId}`;
const listener = createSystemMessageListener(req, {
-
onKill() {
res.end();
},
-
});
res.on('close', () => {
@@ -641,13 +649,14 @@ const startServer = async () => {
// the connection, as there's nothing to stream back
if (!channelName) {
const err = new Error('Unknown channel requested');
+ // @ts-ignore
err.status = 400;
next(err);
return;
}
- accountFromRequest(req).then(() => checkScopes(req, channelName)).then(() => {
+ accountFromRequest(req).then(() => checkScopes(req, req.log, channelName)).then(() => {
subscribeHttpToSystemChannel(req, res);
}).then(() => {
next();
@@ -663,22 +672,28 @@ const startServer = async () => {
* @param {function(Error=): void} next
*/
const errorMiddleware = (err, req, res, next) => {
- log.error(req.requestId, err.toString());
+ req.log.error({ err }, err.toString());
if (res.headersSent) {
next(err);
return;
}
- res.writeHead(err.status || 500, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ error: err.status ? err.toString() : 'An unexpected error occurred' }));
+ const hasStatusCode = Object.hasOwnProperty.call(err, 'status');
+ // @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
* @returns {string}
*/
+ // @ts-ignore
const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', ');
/**
@@ -695,6 +710,7 @@ const startServer = async () => {
return;
}
+ // @ts-ignore
client.query('SELECT id, account_id FROM lists WHERE id = $1 LIMIT 1', [listId], (err, result) => {
done();
@@ -709,8 +725,9 @@ const startServer = async () => {
});
/**
- * @param {string[]} ids
- * @param {any} req
+ * @param {string[]} channelIds
+ * @param {http.IncomingMessage & ResolvedAccount} req
+ * @param {import('pino').Logger} log
* @param {function(string, string): void} output
* @param {undefined | function(string[], SubscriptionListener): void} attachCloseHandler
* @param {'websocket' | 'eventsource'} destinationType
@@ -718,26 +735,34 @@ const startServer = async () => {
* @param {boolean=} allowLocalOnly
* @returns {SubscriptionListener}
*/
- const streamFrom = (ids, req, output, attachCloseHandler, destinationType, needsFiltering = false, allowLocalOnly = false) => {
- const accountId = req.accountId || req.remoteAddress;
-
- log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}`);
+ const streamFrom = (channelIds, req, log, output, attachCloseHandler, destinationType, needsFiltering = false, allowLocalOnly = false) => {
+ log.info({ channelIds }, `Starting stream`);
+ /**
+ * @param {string} event
+ * @param {object|string} payload
+ */
const transmit = (event, payload) => {
// TODO: Replace "string"-based delete payloads with object payloads:
const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
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);
};
// The listener used to process each message off the redis subscription,
// 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.
+
/** @type {SubscriptionListener} */
const listener = message => {
+ if (!message?.event || !message?.payload) {
+ return;
+ }
+
const { event, payload } = message;
// Only send local-only statuses to logged-in users
@@ -766,7 +791,7 @@ const startServer = async () => {
// Filter based on language:
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;
}
@@ -777,6 +802,7 @@ const startServer = async () => {
}
// 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 accountDomain = payload.account.acct.split('@')[1];
@@ -788,6 +814,7 @@ const startServer = async () => {
}
const queries = [
+ // @ts-ignore
client.query(`SELECT 1
FROM blocks
WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)}))
@@ -800,10 +827,13 @@ const startServer = async () => {
];
if (accountDomain) {
+ // @ts-ignore
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) {
+ // @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]));
}
@@ -826,9 +856,11 @@ const startServer = async () => {
// Handling for constructing the custom filters and caching them on the request
// TODO: Move this logic out of the message handling lifecycle
+ // @ts-ignore
if (!req.cachedFilters) {
const filterRows = values[accountDomain ? 2 : 1].rows;
+ // @ts-ignore
req.cachedFilters = filterRows.reduce((cache, filter) => {
if (cache[filter.id]) {
cache[filter.id].keywords.push([filter.keyword, filter.whole_word]);
@@ -858,7 +890,9 @@ const startServer = async () => {
// needs to be done in a separate loop as the database returns one
// filterRow per keyword, so we need all the keywords before
// constructing the regular expression
+ // @ts-ignore
Object.keys(req.cachedFilters).forEach((key) => {
+ // @ts-ignore
req.cachedFilters[key].regexp = new RegExp(req.cachedFilters[key].keywords.map(([keyword, whole_word]) => {
let expr = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -879,13 +913,16 @@ const startServer = async () => {
// Apply cachedFilters against the payload, constructing a
// `filter_results` array of FilterResult entities
+ // @ts-ignore
if (req.cachedFilters) {
const status = payload;
// 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(/
/g, '\n').replace(/<\/p>/g, '\n\n');
const searchableTextContent = JSDOM.fragment(searchableContent).textContent;
const now = new Date();
+ // @ts-ignore
const filter_results = Object.values(req.cachedFilters).reduce((results, cachedFilter) => {
// Check the filter hasn't expired before applying:
if (cachedFilter.expires_at !== null && cachedFilter.expires_at < now) {
@@ -933,12 +970,12 @@ const startServer = async () => {
});
};
- ids.forEach(id => {
+ channelIds.forEach(id => {
subscribe(`${redisPrefix}${id}`, listener);
});
if (typeof attachCloseHandler === 'function') {
- attachCloseHandler(ids.map(id => `${redisPrefix}${id}`), listener);
+ attachCloseHandler(channelIds.map(id => `${redisPrefix}${id}`), listener);
}
return listener;
@@ -950,8 +987,6 @@ const startServer = async () => {
* @returns {function(string, string): void}
*/
const streamToHttp = (req, res) => {
- const accountId = req.accountId || req.remoteAddress;
-
const channelName = channelNameFromPath(req);
connectedClients.labels({ type: 'eventsource' }).inc();
@@ -970,7 +1005,8 @@ const startServer = async () => {
const heartbeat = setInterval(() => res.write(':thump\n'), 15000);
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
// method we don't have knowledge of the channel names
connectedClients.labels({ type: 'eventsource' }).dec();
@@ -1014,15 +1050,15 @@ const startServer = async () => {
*/
const streamToWs = (req, ws, streamName) => (event, payload) => {
if (ws.readyState !== ws.OPEN) {
- log.error(req.requestId, 'Tried writing to closed socket');
+ req.log.error('Tried writing to closed socket');
return;
}
const message = JSON.stringify({ stream: streamName, event, payload });
- ws.send(message, (/** @type {Error} */ err) => {
+ ws.send(message, (/** @type {Error|undefined} */ err) => {
if (err) {
- log.error(req.requestId, `Failed to send to websocket: ${err}`);
+ req.log.error({err}, `Failed to send to websocket`);
}
});
};
@@ -1039,20 +1075,19 @@ const startServer = async () => {
app.use(api);
- api.use(setRequestId);
- api.use(setRemoteAddress);
-
api.use(authenticationMiddleware);
api.use(errorMiddleware);
api.get('/api/v1/streaming/*', (req, res) => {
+ // @ts-ignore
channelNameToIds(req, channelNameFromPath(req), req.query).then(({ channelIds, options }) => {
const onSend = streamToHttp(req, res);
const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds));
- streamFrom(channelIds, req, onSend, onEnd, 'eventsource', options.needsFiltering, options.allowLocalOnly);
+ // @ts-ignore
+ streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering, options.allowLocalOnly);
}).catch(err => {
- log.verbose(req.requestId, 'Subscription error:', err.toString());
+ res.log.info({ err }, 'Subscription error:', err.toString());
httpNotFound(res);
});
});
@@ -1082,34 +1117,6 @@ const startServer = async () => {
return arr;
};
- /**
- * See app/lib/ascii_folder.rb for the canon definitions
- * of these constants
- */
- const NON_ASCII_CHARS = 'ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšſŢţŤťŦŧÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųŴŵÝýÿŶŷŸŹźŻżŽž';
- const EQUIVALENT_ASCII_CHARS = 'AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSssTtTtTtUUUUuuuuUuUuUuUuUuUuWwYyyYyYZzZzZz';
-
- /**
- * @param {string} str
- * @returns {string}
- */
- const foldToASCII = str => {
- const regex = new RegExp(NON_ASCII_CHARS.split('').join('|'), 'g');
-
- return str.replace(regex, match => {
- const index = NON_ASCII_CHARS.indexOf(match);
- return EQUIVALENT_ASCII_CHARS[index];
- });
- };
-
- /**
- * @param {string} str
- * @returns {string}
- */
- const normalizeHashtag = str => {
- return foldToASCII(str.normalize('NFKC').toLowerCase()).replace(/[^\p{L}\p{N}_\u00b7\u200c]/gu, '');
- };
-
/**
* @param {any} req
* @param {string} name
@@ -1218,6 +1225,7 @@ const startServer = async () => {
break;
case 'list':
+ // @ts-ignore
authorizeListAccess(params.list, req).then(() => {
resolve({
channelIds: [`timeline:list:${params.list}`],
@@ -1239,9 +1247,9 @@ const startServer = async () => {
* @returns {string[]}
*/
const streamNameFromChannelName = (channelName, params) => {
- if (channelName === 'list') {
+ if (channelName === 'list' && 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];
} else {
return [channelName];
@@ -1250,8 +1258,9 @@ const startServer = async () => {
/**
* @typedef WebSocketSession
- * @property {WebSocket} websocket
- * @property {http.IncomingMessage} request
+ * @property {WebSocket & { isAlive: boolean}} websocket
+ * @property {http.IncomingMessage & ResolvedAccount} request
+ * @property {import('pino').Logger} logger
* @property {Object.} subscriptions
*/
@@ -1261,8 +1270,8 @@ const startServer = async () => {
* @param {StreamParams} params
* @returns {void}
*/
- const subscribeWebsocketToChannel = ({ socket, request, subscriptions }, channelName, params) => {
- checkScopes(request, channelName).then(() => channelNameToIds(request, channelName, params)).then(({
+ const subscribeWebsocketToChannel = ({ websocket, request, logger, subscriptions }, channelName, params) => {
+ checkScopes(request, logger, channelName).then(() => channelNameToIds(request, channelName, params)).then(({
channelIds,
options,
}) => {
@@ -1270,9 +1279,9 @@ const startServer = async () => {
return;
}
- const onSend = streamToWs(request, socket, streamNameFromChannelName(channelName, params));
+ const onSend = streamToWs(request, websocket, streamNameFromChannelName(channelName, params));
const stopHeartbeat = subscriptionHeartbeat(channelIds);
- const listener = streamFrom(channelIds, request, onSend, undefined, 'websocket', options.needsFiltering, options.allowLocalOnly);
+ const listener = streamFrom(channelIds, request, logger, onSend, undefined, 'websocket', options.needsFiltering, options.allowLocalOnly);
connectedChannels.labels({ type: 'websocket', channel: channelName }).inc();
@@ -1282,14 +1291,17 @@ const startServer = async () => {
stopHeartbeat,
};
}).catch(err => {
- log.verbose(request.requestId, 'Subscription error:', err.toString());
- socket.send(JSON.stringify({ error: err.toString() }));
+ logger.error({ err }, 'Subscription error');
+ websocket.send(JSON.stringify({ error: err.toString() }));
});
};
-
- const removeSubscription = (subscriptions, channelIds, request) => {
- log.verbose(request.requestId, `Ending stream from ${channelIds.join(', ')} for ${request.accountId}`);
+ /**
+ * @param {WebSocketSession} session
+ * @param {string[]} channelIds
+ */
+ const removeSubscription = ({ request, logger, subscriptions }, channelIds) => {
+ logger.info({ channelIds, accountId: request.accountId }, `Ending stream`);
const subscription = subscriptions[channelIds.join(';')];
@@ -1313,16 +1325,17 @@ const startServer = async () => {
* @param {StreamParams} params
* @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 }) => {
- removeSubscription(subscriptions, channelIds, request);
+ removeSubscription(session, channelIds);
}).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:
- // FIXME: In other parts of the code ws === socket
- if (socket.isAlive && socket.readyState === socket.OPEN) {
- socket.send(JSON.stringify({ error: "Error unsubscribing from channel" }));
+ if (websocket.isAlive && websocket.readyState === websocket.OPEN) {
+ websocket.send(JSON.stringify({ error: "Error unsubscribing from channel" }));
}
});
};
@@ -1330,16 +1343,14 @@ const startServer = async () => {
/**
* @param {WebSocketSession} session
*/
- const subscribeWebsocketToSystemChannel = ({ socket, request, subscriptions }) => {
+ const subscribeWebsocketToSystemChannel = ({ websocket, request, subscriptions }) => {
const accessTokenChannelId = `timeline:access_token:${request.accessTokenId}`;
const systemChannelId = `timeline:system:${request.accountId}`;
const listener = createSystemMessageListener(request, {
-
onKill() {
- socket.close();
+ websocket.close();
},
-
});
subscribe(`${redisPrefix}${accessTokenChannelId}`, listener);
@@ -1362,32 +1373,17 @@ const startServer = async () => {
connectedChannels.labels({ type: 'websocket', channel: 'system' }).inc(2);
};
- /**
- * @param {string|string[]} arrayOrString
- * @returns {string}
- */
- const firstParam = arrayOrString => {
- if (Array.isArray(arrayOrString)) {
- return arrayOrString[0];
- } else {
- return arrayOrString;
- }
- };
-
/**
* @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
// increment the connected clients metric straight away when we establish
// the connection, without waiting:
connectedClients.labels({ type: 'websocket' }).inc();
- // Setup request properties:
- req.requestId = uuid.v4();
- req.remoteAddress = ws._socket.remoteAddress;
-
// Setup connection keep-alive state:
ws.isAlive = true;
ws.on('pong', () => {
@@ -1398,8 +1394,9 @@ const startServer = async () => {
* @type {WebSocketSession}
*/
const session = {
- socket: ws,
+ websocket: ws,
request: req,
+ logger: log,
subscriptions: {},
};
@@ -1407,27 +1404,30 @@ const startServer = async () => {
const subscriptions = Object.keys(session.subscriptions);
subscriptions.forEach(channelIds => {
- removeSubscription(session.subscriptions, channelIds.split(';'), req);
+ removeSubscription(session, channelIds.split(';'));
});
// Decrement the metrics for connected clients:
connectedClients.labels({ type: 'websocket' }).dec();
- // ensure garbage collection:
- session.socket = null;
- session.request = null;
- session.subscriptions = {};
+ // We need to delete the session object as to ensure it correctly gets
+ // garbage collected, without doing this we could accidentally hold on to
+ // references to the websocket, the request, and the logger, causing
+ // memory leaks.
+ //
+ // @ts-ignore
+ delete session;
});
// 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.
- ws.on('error', (err) => {
- log.error('websocket', err.toString());
+ ws.on('error', (/** @type {Error} */ err) => {
+ log.error(err);
});
ws.on('message', (data, 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');
return;
}
@@ -1462,18 +1462,20 @@ const startServer = async () => {
setInterval(() => {
wss.clients.forEach(ws => {
+ // @ts-ignore
if (ws.isAlive === false) {
ws.terminate();
return;
}
+ // @ts-ignore
ws.isAlive = false;
ws.ping('', false);
});
}, 30000);
attachServerWithConfig(server, address => {
- log.warn(`Streaming API now listening on ${address}`);
+ logger.info(`Streaming API now listening on ${address}`);
});
const onExit = () => {
@@ -1481,8 +1483,10 @@ const startServer = async () => {
process.exit(0);
};
+ /** @param {Error} err */
const onError = (err) => {
- log.error(err);
+ logger.error(err);
+
server.close();
process.exit(0);
};
@@ -1506,7 +1510,7 @@ const attachServerWithConfig = (server, onSuccess) => {
}
});
} 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) {
onSuccess(`${server.address().address}:${server.address().port}`);
}
diff --git a/streaming/logging.js b/streaming/logging.js
new file mode 100644
index 0000000000..64ee474875
--- /dev/null
+++ b/streaming/logging.js
@@ -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} 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';
+ }
+};
diff --git a/streaming/package.json b/streaming/package.json
index 149055ca1b..3f76e25786 100644
--- a/streaming/package.json
+++ b/streaming/package.json
@@ -20,10 +20,11 @@
"dotenv": "^16.0.3",
"express": "^4.18.2",
"ioredis": "^5.3.2",
- "jsdom": "^23.0.0",
- "npmlog": "^7.0.1",
+ "jsdom": "^24.0.0",
"pg": "^8.5.0",
"pg-connection-string": "^2.6.0",
+ "pino": "^8.17.2",
+ "pino-http": "^9.0.0",
"prom-client": "^15.0.0",
"uuid": "^9.0.0",
"ws": "^8.12.1"
@@ -31,11 +32,11 @@
"devDependencies": {
"@types/cors": "^2.8.16",
"@types/express": "^4.17.17",
- "@types/npmlog": "^7.0.0",
"@types/pg": "^8.6.6",
"@types/uuid": "^9.0.0",
"@types/ws": "^8.5.9",
"eslint-define-config": "^2.0.0",
+ "pino-pretty": "^10.3.1",
"typescript": "^5.0.4"
},
"optionalDependencies": {
diff --git a/streaming/utils.js b/streaming/utils.js
index ad8dd4889f..7b87a1d14c 100644
--- a/streaming/utils.js
+++ b/streaming/utils.js
@@ -20,3 +20,50 @@ const isTruthy = value =>
value && !FALSE_VALUES.includes(value);
exports.isTruthy = isTruthy;
+
+
+/**
+ * See app/lib/ascii_folder.rb for the canon definitions
+ * of these constants
+ */
+const NON_ASCII_CHARS = 'ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšſŢţŤťŦŧÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųŴŵÝýÿŶŷŸŹźŻżŽž';
+const EQUIVALENT_ASCII_CHARS = 'AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSssTtTtTtUUUUuuuuUuUuUuUuUuUuWwYyyYyYZzZzZz';
+
+/**
+ * @param {string} str
+ * @returns {string}
+ */
+function foldToASCII(str) {
+ const regex = new RegExp(NON_ASCII_CHARS.split('').join('|'), 'g');
+
+ return str.replace(regex, function(match) {
+ const index = NON_ASCII_CHARS.indexOf(match);
+ return EQUIVALENT_ASCII_CHARS[index];
+ });
+}
+
+exports.foldToASCII = foldToASCII;
+
+/**
+ * @param {string} str
+ * @returns {string}
+ */
+function normalizeHashtag(str) {
+ return foldToASCII(str.normalize('NFKC').toLowerCase()).replace(/[^\p{L}\p{N}_\u00b7\u200c]/gu, '');
+}
+
+exports.normalizeHashtag = normalizeHashtag;
+
+/**
+ * @param {string|string[]} arrayOrString
+ * @returns {string}
+ */
+function firstParam(arrayOrString) {
+ if (Array.isArray(arrayOrString)) {
+ return arrayOrString[0];
+ } else {
+ return arrayOrString;
+ }
+}
+
+exports.firstParam = firstParam;
diff --git a/yarn.lock b/yarn.lock
index 5864c1a3f9..26c1de2b75 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -42,17 +42,6 @@ __metadata:
languageName: node
linkType: hard
-"@asamuzakjp/dom-selector@npm:^2.0.1":
- version: 2.0.1
- resolution: "@asamuzakjp/dom-selector@npm:2.0.1"
- dependencies:
- bidi-js: "npm:^1.0.3"
- css-tree: "npm:^2.3.1"
- is-potential-custom-element-name: "npm:^1.0.1"
- checksum: 232895f16f2f9dfc637764df2529084d16e1c122057766a79b16e1d40808e09fffae28c0f0cc8376f8a1564a85dba9d4b2f140a9a0b65f4f95c960192b797037
- languageName: node
- linkType: hard
-
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.23.5":
version: 7.23.5
resolution: "@babel/code-frame@npm:7.23.5"
@@ -2539,7 +2528,6 @@ __metadata:
dependencies:
"@types/cors": "npm:^2.8.16"
"@types/express": "npm:^4.17.17"
- "@types/npmlog": "npm:^7.0.0"
"@types/pg": "npm:^8.6.6"
"@types/uuid": "npm:^9.0.0"
"@types/ws": "npm:^8.5.9"
@@ -2549,10 +2537,12 @@ __metadata:
eslint-define-config: "npm:^2.0.0"
express: "npm:^4.18.2"
ioredis: "npm:^5.3.2"
- jsdom: "npm:^23.0.0"
- npmlog: "npm:^7.0.1"
+ jsdom: "npm:^24.0.0"
pg: "npm:^8.5.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"
typescript: "npm:^5.0.4"
utf-8-validate: "npm:^6.0.3"
@@ -3341,15 +3331,6 @@ __metadata:
languageName: node
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":
version: 4.0.33
resolution: "@types/object-assign@npm:4.0.33"
@@ -3555,13 +3536,13 @@ __metadata:
linkType: hard
"@types/react@npm:*, @types/react@npm:16 || 17 || 18, @types/react@npm:>=16.9.11, @types/react@npm:^18.2.7":
- version: 18.2.47
- resolution: "@types/react@npm:18.2.47"
+ version: 18.2.48
+ resolution: "@types/react@npm:18.2.48"
dependencies:
"@types/prop-types": "npm:*"
"@types/scheduler": "npm:*"
csstype: "npm:^3.0.2"
- checksum: e98ea1827fe60636d0f7ce206397159a29fc30613fae43e349e32c10ad3c0b7e0ed2ded2f3239e07bd5a3cba8736b6114ba196acccc39905ca4a06f56a8d2841
+ checksum: 7e89f18ea2928b1638f564b156d692894dcb9352a7e0a807873c97e858abe1f23dbd165a25dd088a991344e973fdeef88ba5724bfb64504b74072cbc9c220c3a
languageName: node
linkType: hard
@@ -4327,13 +4308,6 @@ __metadata:
languageName: node
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":
version: 0.0.2
resolution: "are-docs-informative@npm:0.0.2"
@@ -4341,16 +4315,6 @@ __metadata:
languageName: node
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":
version: 1.0.10
resolution: "argparse@npm:1.0.10"
@@ -4672,6 +4636,13 @@ __metadata:
languageName: node
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
+
"atrament@npm:0.2.4":
version: 0.2.4
resolution: "atrament@npm:0.2.4"
@@ -4976,15 +4947,6 @@ __metadata:
languageName: node
linkType: hard
-"bidi-js@npm:^1.0.3":
- version: 1.0.3
- resolution: "bidi-js@npm:1.0.3"
- dependencies:
- require-from-string: "npm:^2.0.2"
- checksum: fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1
- languageName: node
- linkType: hard
-
"big-integer@npm:^1.6.44":
version: 1.6.51
resolution: "big-integer@npm:1.6.51"
@@ -5773,15 +5735,6 @@ __metadata:
languageName: node
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":
version: 2.9.3
resolution: "colord@npm:2.9.3"
@@ -5789,7 +5742,7 @@ __metadata:
languageName: node
linkType: hard
-"colorette@npm:^2.0.20":
+"colorette@npm:^2.0.20, colorette@npm:^2.0.7":
version: 2.0.20
resolution: "colorette@npm:2.0.20"
checksum: e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40
@@ -5921,13 +5874,6 @@ __metadata:
languageName: node
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":
version: 1.0.0
resolution: "constants-browserify@npm:1.0.0"
@@ -6003,9 +5949,9 @@ __metadata:
linkType: hard
"core-js@npm:^3.30.2":
- version: 3.35.0
- resolution: "core-js@npm:3.35.0"
- checksum: 1d545ff4406f2afa5e681f44b45ed5f7f119d158b380234d5aa7787ce7e47fc7a635b98b74c28c766ba8191e3db8c2316ad6ab4ff1ddecbc3fd618413a52c29c
+ version: 3.35.1
+ resolution: "core-js@npm:3.35.1"
+ checksum: ebc8e22c36d13bcf2140cbc1d8ad65d1b08192bff4c43ade70c72eac103cb4dcfbc521f2b1ad1c74881b0a4353e64986537893ae4f07888e49228340efa13ae6
languageName: node
linkType: hard
@@ -6455,6 +6401,13 @@ __metadata:
languageName: node
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":
version: 1.2.1
resolution: "debounce@npm:1.2.1"
@@ -6690,13 +6643,6 @@ __metadata:
languageName: node
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":
version: 2.1.0
resolution: "denque@npm:2.1.0"
@@ -6965,9 +6911,9 @@ __metadata:
linkType: hard
"dotenv@npm:^16.0.3":
- version: 16.3.1
- resolution: "dotenv@npm:16.3.1"
- checksum: b95ff1bbe624ead85a3cd70dbd827e8e06d5f05f716f2d0cbc476532d54c7c9469c3bc4dd93ea519f6ad711cb522c00ac9a62b6eb340d5affae8008facc3fbd7
+ version: 16.3.2
+ resolution: "dotenv@npm:16.3.2"
+ checksum: a87d62cef0810b670cb477db1a24a42a093b6b428c9e65c185ce1d6368ad7175234b13547718ba08da18df43faae4f814180cc0366e11be1ded2277abc4dd22e
languageName: node
linkType: hard
@@ -7969,6 +7915,13 @@ __metadata:
languageName: node
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":
version: 3.1.3
resolution: "fast-deep-equal@npm:3.1.3"
@@ -8010,6 +7963,20 @@ __metadata:
languageName: node
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":
version: 1.0.16
resolution: "fastest-levenshtein@npm:1.0.16"
@@ -8431,22 +8398,6 @@ __metadata:
languageName: node
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":
version: 1.0.0-beta.2
resolution: "gensync@npm:1.0.0-beta.2"
@@ -8795,13 +8746,6 @@ __metadata:
languageName: node
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":
version: 0.3.1
resolution: "has-value@npm:0.3.1"
@@ -8878,6 +8822,13 @@ __metadata:
languageName: node
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":
version: 4.10.1
resolution: "history@npm:4.10.1"
@@ -10594,6 +10545,13 @@ __metadata:
languageName: node
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":
version: 7.1.1
resolution: "jpeg-autorotate@npm:7.1.1"
@@ -10692,11 +10650,10 @@ __metadata:
languageName: node
linkType: hard
-"jsdom@npm:^23.0.0":
- version: 23.2.0
- resolution: "jsdom@npm:23.2.0"
+"jsdom@npm:^24.0.0":
+ version: 24.0.0
+ resolution: "jsdom@npm:24.0.0"
dependencies:
- "@asamuzakjp/dom-selector": "npm:^2.0.1"
cssstyle: "npm:^4.0.1"
data-urls: "npm:^5.0.0"
decimal.js: "npm:^10.4.3"
@@ -10705,6 +10662,7 @@ __metadata:
http-proxy-agent: "npm:^7.0.0"
https-proxy-agent: "npm:^7.0.2"
is-potential-custom-element-name: "npm:^1.0.1"
+ nwsapi: "npm:^2.2.7"
parse5: "npm:^7.1.2"
rrweb-cssom: "npm:^0.6.0"
saxes: "npm:^6.0.0"
@@ -10722,7 +10680,7 @@ __metadata:
peerDependenciesMeta:
canvas:
optional: true
- checksum: b062af50f7be59d914ba75236b7817c848ef3cd007aea1d6b8020a41eb263b7d5bd2652298106e9756b56892f773d990598778d02adab7d0d0d8e58726fc41d3
+ checksum: 7b35043d7af39ad6dcaef0fa5679d8c8a94c6c9b6cc4a79222b7c9987d57ab7150c50856684ae56b473ab28c7d82aec0fb7ca19dcbd4c3f46683c807d717a3af
languageName: node
linkType: hard
@@ -11990,18 +11948,6 @@ __metadata:
languageName: node
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":
version: 1.0.2
resolution: "nth-check@npm:1.0.2"
@@ -12020,7 +11966,7 @@ __metadata:
languageName: node
linkType: hard
-"nwsapi@npm:^2.2.2":
+"nwsapi@npm:^2.2.2, nwsapi@npm:^2.2.7":
version: 2.2.7
resolution: "nwsapi@npm:2.2.7"
checksum: 44be198adae99208487a1c886c0a3712264f7bbafa44368ad96c003512fed2753d4e22890ca1e6edb2690c3456a169f2a3c33bfacde1905cf3bf01c7722464db
@@ -12174,6 +12120,13 @@ __metadata:
languageName: node
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":
version: 2.4.1
resolution: "on-finished@npm:2.4.1"
@@ -12741,6 +12694,80 @@ __metadata:
languageName: node
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":
version: 4.0.6
resolution: "pirates@npm:4.0.6"
@@ -13343,6 +13370,13 @@ __metadata:
languageName: node
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":
version: 0.11.10
resolution: "process@npm:0.11.10"
@@ -13520,6 +13554,13 @@ __metadata:
languageName: node
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":
version: 3.4.1
resolution: "raf@npm:3.4.1"
@@ -13762,8 +13803,8 @@ __metadata:
linkType: hard
"react-redux@npm:^9.0.4":
- version: 9.0.4
- resolution: "react-redux@npm:9.0.4"
+ version: 9.1.0
+ resolution: "react-redux@npm:9.1.0"
dependencies:
"@types/use-sync-external-store": "npm:^0.0.3"
use-sync-external-store: "npm:^1.0.0"
@@ -13779,7 +13820,7 @@ __metadata:
optional: true
redux:
optional: true
- checksum: 23af10014b129aeb051de729bde01de21175170b860deefb7ad83483feab5816253f770a4cea93333fc22a53ac9ac699b27f5c3705c388dab53dbcb2906a571a
+ checksum: 53161b5dc4d109020fbc42d26906ace92fed9ba1d7ab6274af60e9c0684583d20d1c8ec6d58601ac7b833c6468a652bbf3d4a102149d1793cb8a28b05b042f73
languageName: node
linkType: hard
@@ -14015,15 +14056,16 @@ __metadata:
languageName: node
linkType: hard
-"readable-stream@npm:^4.1.0":
- version: 4.4.0
- resolution: "readable-stream@npm:4.4.0"
+"readable-stream@npm:^4.0.0":
+ version: 4.4.2
+ resolution: "readable-stream@npm:4.4.2"
dependencies:
abort-controller: "npm:^3.0.0"
buffer: "npm:^6.0.3"
events: "npm:^3.3.0"
process: "npm:^0.11.10"
- checksum: 83f5a11285e5ebefb7b22a43ea77a2275075639325b4932a328a1fb0ee2475b83b9cc94326724d71c6aa3b60fa87e2b16623530b1cac34f3825dcea0996fdbe4
+ string_decoder: "npm:^1.3.0"
+ checksum: cf7cc8daa2b57872d120945a20a1458c13dcb6c6f352505421115827b18ac4df0e483ac1fe195cb1f5cd226e1073fc55b92b569269d8299e8530840bcdbba40c
languageName: node
linkType: hard
@@ -14047,6 +14089,13 @@ __metadata:
languageName: node
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":
version: 3.0.0
resolution: "redent@npm:3.0.0"
@@ -14592,6 +14641,13 @@ __metadata:
languageName: node
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":
version: 2.1.2
resolution: "safer-buffer@npm:2.1.2"
@@ -14625,15 +14681,15 @@ __metadata:
linkType: hard
"sass@npm:^1.62.1":
- version: 1.69.7
- resolution: "sass@npm:1.69.7"
+ version: 1.70.0
+ resolution: "sass@npm:1.70.0"
dependencies:
chokidar: "npm:>=3.0.0 <4.0.0"
immutable: "npm:^4.0.0"
source-map-js: "npm:>=0.6.2 <2.0.0"
bin:
sass: sass.js
- checksum: 773d0938e7d4ff3972d3fda3132f34fe98a2f712e028a58e28fecd615434795eff3266eddc38d5e13f03b90c0d6360d0e737b30bff2949a47280c64a18e0fb18
+ checksum: 7c309ee1c096d591746d122da9f1ebd65b4c4b3a60c2cc0ec720fd98fe1205fa8b44c9f563d113b9fdfeb25af1e32ec9b3e048bd4b8e05d267f020953bd7baf0
languageName: node
linkType: hard
@@ -14705,6 +14761,13 @@ __metadata:
languageName: node
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":
version: 2.0.0
resolution: "select-hose@npm:2.0.0"
@@ -15108,6 +15171,15 @@ __metadata:
languageName: node
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":
version: 2.0.1
resolution: "source-list-map@npm:2.0.1"
@@ -15266,7 +15338,7 @@ __metadata:
languageName: node
linkType: hard
-"split2@npm:^4.1.0":
+"split2@npm:^4.0.0, split2@npm:^4.1.0":
version: 4.2.0
resolution: "split2@npm:4.2.0"
checksum: b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534
@@ -15431,7 +15503,7 @@ __metadata:
languageName: node
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
resolution: "string-width@npm:4.2.3"
dependencies:
@@ -15524,7 +15596,7 @@ __metadata:
languageName: node
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
resolution: "string_decoder@npm:1.3.0"
dependencies:
@@ -16070,6 +16142,15 @@ __metadata:
languageName: node
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":
version: 1.1.0
resolution: "thunky@npm:1.1.0"
@@ -17307,15 +17388,6 @@ __metadata:
languageName: node
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":
version: 2.0.1
resolution: "wildcard@npm:2.0.1"