Add magic links

This commit is contained in:
Alice Gaudon 2020-05-09 23:19:47 +02:00
parent 26fdc94e9b
commit e574b6e778
6 changed files with 609 additions and 7 deletions

View File

@ -1,6 +1,6 @@
{
"name": "wms-core",
"version": "0.4.26",
"version": "0.4.28",
"description": "Node web framework",
"repository": "git@gitlab.com:ArisuOngaku/wms-core.git",
"author": "Alice Gaudon <alice@gaudon.pro>",
@ -38,6 +38,7 @@
"@types/nunjucks": "^3.1.3",
"@types/redis": "^2.8.18",
"@types/ws": "^7.2.4",
"argon2": "^0.26.2",
"config": "^3.3.1",
"connect-flash": "^0.1.1",
"connect-redis": "^4.0.4",

View File

@ -0,0 +1,110 @@
import Controller from "../../Controller";
import {Request, Response} from "express";
import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener";
import {BadRequestError, NotFoundHttpError} from "../../HttpError";
import querystring from "querystring";
import Throttler from "../../Throttler";
import Mail, {MailTemplate} from "../../Mail";
import MagicLink from "../models/MagicLink";
import config from "config";
export default abstract class MagicLinkController extends Controller {
public static async sendMagicLink(sessionID: string, actionType: string, email: string, mailTemplate: MailTemplate, data: object, req: Request, res: Response): Promise<void> {
Throttler.throttle('magic_link', 2, MagicLink.validityPeriod(), sessionID, 0, 0);
Throttler.throttle('magic_link', 1, MagicLink.validityPeriod(), email, 0, 0);
let link = await MagicLink.bySessionID(sessionID, actionType);
if (!link) {
link = new MagicLink({
session_id: sessionID,
});
}
const token = await link.generateToken(email);
await link.save();
// Send email
await new Mail(mailTemplate, Object.assign(data, {
link: `${req.protocol}://${req.get('host') + Controller.route('magic_link')}?${querystring.stringify({
id: link.id,
token: token,
})}`,
})).send(email);
res.redirect(Controller.route('magic_link_lobby'));
}
protected readonly magicLinkWebsocketPath: string;
protected constructor(magicLinkWebsocketListener: MagicLinkWebSocketListener) {
super();
this.magicLinkWebsocketPath = magicLinkWebsocketListener.path();
}
getRoutesPrefix(): string {
return '/magic';
}
routes(): void {
this.get('/lobby', this.getLobby, 'magic_link_lobby');
this.get('/link', this.getMagicLink, 'magic_link');
}
private async getLobby(req: Request, res: Response): Promise<void> {
const link = await MagicLink.bySessionID(req.sessionID!);
if (!link) {
throw new NotFoundHttpError('magic link', req.url);
}
if (!await link.isValid()) {
req.flash('error', 'This magic link has expired. Please try again.');
res.redirect(link.getOriginalURL());
return;
}
if (await link.isAuthorized()) {
await this.performAction(link, req, res);
return;
}
res.render('magic_link_lobby', {
email: link.getEmail(),
type: await link.getActionType(),
validUntil: link.getExpirationDate().getTime(),
websocketUrl: config.get<string>('public_websocket_url') + this.magicLinkWebsocketPath,
});
}
private async getMagicLink(req: Request, res: Response): Promise<void> {
const id = parseInt(<string>req.query.id);
const token = <string>req.query.token;
if (!id || !token) throw new BadRequestError('Need parameters id, token.', 'Please try again.', req.originalUrl);
let success = true;
let err;
const magicLink = await MagicLink.getById<MagicLink>(id);
if (!magicLink) {
res.status(404);
err = `Couldn't find this magic link. Perhaps it has already expired.`;
success = false;
} else if (!await magicLink.isAuthorized()) {
err = await magicLink.verifyToken(token);
if (err === null) {
// Validation success, authenticate the user
magicLink.authorize();
await magicLink.save();
MagicLinkWebSocketListener.refreshMagicLink(magicLink.getSessionID());
}
}
res.render('magic_link', {
actionType: await magicLink?.getActionType(),
actionMessage: await magicLink?.getActionMessage(),
err: err,
success: success && err === null,
});
}
protected abstract async performAction(magicLink: MagicLink, req: Request, res: Response): Promise<void>;
}

View File

@ -0,0 +1,56 @@
import WebSocket from "ws";
import {IncomingMessage} from "http";
import WebSocketListener from "../../WebSocketListener";
import MagicLink from "../models/MagicLink";
export default class MagicLinkWebSocketListener extends WebSocketListener {
private static readonly connections: { [p: string]: (() => void)[] } = {};
public static refreshMagicLink(sessionID: string) {
const fs = MagicLinkWebSocketListener.connections[sessionID];
if (fs) {
fs.forEach(f => f());
}
}
async handle(socket: WebSocket, request: IncomingMessage, session: Express.SessionData): Promise<void> {
// Refuse any incoming data
socket.on('message', () => {
socket.close(1003);
});
// Get magic link
const magicLink = await MagicLink.bySessionID(session.id);
// Refresh if immediately applicable
if (!magicLink || !await magicLink.isValid() || await magicLink.isAuthorized()) {
socket.send('refresh');
socket.close(1000);
return;
}
const validityTimeout = setTimeout(() => {
socket.send('refresh');
socket.close(1000);
}, magicLink.getExpirationDate().getTime() - new Date().getTime());
const f = () => {
clearTimeout(validityTimeout);
socket.send('refresh');
socket.close(1000);
};
socket.on('close', () => {
MagicLinkWebSocketListener.connections[session.id] = MagicLinkWebSocketListener.connections[session.id].filter(f => f !== f);
if (MagicLinkWebSocketListener.connections[session.id].length === 0) delete MagicLinkWebSocketListener.connections[session.id];
});
if (!MagicLinkWebSocketListener.connections[session.id]) MagicLinkWebSocketListener.connections[session.id] = [];
MagicLinkWebSocketListener.connections[session.id].push(f);
}
path(): string {
return '/magic-link';
}
}

View File

@ -0,0 +1,23 @@
import Migration from "../../db/Migration";
import {query} from "../../db/MysqlConnectionManager";
export default class CreateMagicLinksTable extends Migration {
async install(): Promise<void> {
await query('CREATE TABLE magic_links(' +
'id INT NOT NULL AUTO_INCREMENT,' +
'session_id CHAR(32) UNIQUE NOT NULL,' +
'email VARCHAR(254) NOT NULL,' +
'token CHAR(96) NOT NULL,' +
'action_type VARCHAR(64) NOT NULL,' +
'original_url VARCHAR(1745) NOT NULL,' +
'generated_at DATETIME NOT NULL,' +
'authorized BOOLEAN NOT NULL,' +
'PRIMARY KEY(id)' +
')');
}
async rollback(): Promise<void> {
await query('DROP TABLE magic_links');
}
}

View File

@ -0,0 +1,125 @@
import crypto from "crypto";
import config from "config";
import Model, {EMAIL_REGEX} from "../../db/Model";
import AuthProof from "../AuthProof";
import Validator from "../../db/Validator";
import User from "./User";
import argon2 from "argon2";
export default class MagicLink extends Model implements AuthProof {
public static async bySessionID(sessionID: string, actionType?: string | string[]): Promise<MagicLink | null> {
let query = this.select().where('session_id', sessionID);
if (actionType !== undefined) {
if (typeof actionType === 'string') {
query = query.where('action_type', actionType);
} else {
query = query.whereIn('action_type', actionType);
}
}
const links = await this.models<MagicLink>(query.first());
return links.length > 0 ? links[0] : null;
}
public static validityPeriod(): number {
return config.get<number>('magic_link.validity_period') * 1000;
}
private session_id?: string;
private email?: string;
private token?: string;
private action_type?: string;
private original_url?: string;
private generated_at?: Date;
private authorized?: boolean;
constructor(data: any) {
super(data);
if (this.action_type === undefined) throw new Error('Action type must always be defined.');
if (this.original_url === undefined) throw new Error('Origin url must always be defined.');
if (this.authorized === undefined) {
this.authorized = false;
}
}
protected defineProperties(): void {
this.defineProperty<string>('session_id', new Validator().defined().length(32).unique(this));
this.defineProperty<string>('email', new Validator().defined().regexp(EMAIL_REGEX));
this.defineProperty<string>('token', new Validator().defined().length(96));
this.defineProperty<string>('action_type', new Validator().defined().maxLength(64));
this.defineProperty<string>('original_url', new Validator().defined().maxLength(1745));
this.defineProperty<Date>('generated_at', new Validator());
this.defineProperty<boolean>('authorized', new Validator().defined());
}
public async isOwnedBy(userId: number): Promise<boolean> {
const user = await this.getUser();
return user !== null && user.id === userId;
}
public async getUser(): Promise<User | null> {
return await User.fromEmail(await this.getEmail());
}
public async revoke(): Promise<void> {
await this.delete();
}
public async isValid(): Promise<boolean> {
if (await this.isAuthorized()) return true;
return new Date().getTime() < this.getExpirationDate().getTime();
}
public async isAuthorized(): Promise<boolean> {
return this.authorized!;
}
public async generateToken(email: string): Promise<string> {
const rawToken = crypto.randomBytes(48).toString('base64'); // Raw token length = 64
this.email = email;
this.generated_at = new Date();
this.token = await argon2.hash(rawToken, {
timeCost: 10,
memoryCost: 4096,
parallelism: 4
});
return rawToken;
}
/**
* @returns {@code null} if the token is valid, an error {@code string} otherwise.
*/
public async verifyToken(tokenGuess: string): Promise<string | null> {
if (this.token === undefined || this.generated_at === undefined) return 'This token was not generated.'; // There is no token
if (new Date().getTime() - this.generated_at.getTime() > MagicLink.validityPeriod()) return 'This token has expired.'; // Token has expired
if (!await argon2.verify(this.token, tokenGuess)) return 'This token is invalid.';
return null;
}
public getSessionID(): string {
return this.session_id!;
}
public async getEmail(): Promise<string> {
return this.email!;
}
public getActionType(): string {
return this.action_type!;
}
public getOriginalURL(): string {
return this.original_url!;
}
public getExpirationDate(): Date {
if (!this.generated_at) return new Date();
return new Date(this.generated_at?.getTime() + MagicLink.validityPeriod());
}
public authorize() {
this.authorized = true;
}
}

299
yarn.lock
View File

@ -448,6 +448,13 @@
"@types/yargs" "^15.0.0"
chalk "^3.0.0"
"@phc/format@^0.5.0":
version "0.5.0"
resolved "https://registry.toot.party/@phc%2fformat/-/format-0.5.0.tgz#a99d27a83d78b3100a191412adda04315e2e3aba"
integrity sha512-JWtZ5P1bfXU0bAtTzCpOLYHDXuxSVdtL/oqz4+xa97h8w9E5IlVN333wugXVFv8vZ1hbXObKQf1ptXmFFcMByg==
dependencies:
safe-buffer "^5.1.2"
"@sinonjs/commons@^1.7.0":
version "1.7.2"
resolved "https://registry.toot.party/@sinonjs%2fcommons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
@ -778,6 +785,16 @@ ansi-escapes@^4.2.1:
dependencies:
type-fest "^0.11.0"
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.toot.party/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.toot.party/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
ansi-regex@^4.1.0:
version "4.1.0"
resolved "https://registry.toot.party/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
@ -819,11 +836,33 @@ anymatch@^3.0.3, anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
aproba@^1.0.3:
version "1.2.0"
resolved "https://registry.toot.party/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
are-we-there-yet@~1.1.2:
version "1.1.5"
resolved "https://registry.toot.party/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
dependencies:
delegates "^1.0.0"
readable-stream "^2.0.6"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.toot.party/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
argon2@^0.26.2:
version "0.26.2"
resolved "https://registry.toot.party/argon2/-/argon2-0.26.2.tgz#16c62637f79f8511d666c9a824dd991aa3a7731c"
integrity sha512-Tk9I/r3KIHCIHU5x2UawKsPi+g7MByAYnUZghXztQDXRp/997P31wa4qvdvokTaFBpsu6jOZACd+2qkBGGssRA==
dependencies:
"@phc/format" "^0.5.0"
node-addon-api "^2.0.0"
node-pre-gyp "^0.14.0"
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.toot.party/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@ -1201,6 +1240,11 @@ chokidar@^3.0.0, chokidar@^3.3.0:
optionalDependencies:
fsevents "~2.1.2"
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.toot.party/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.toot.party/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
@ -1246,6 +1290,11 @@ co@^4.6.0:
resolved "https://registry.toot.party/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.toot.party/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
collect-v8-coverage@^1.0.0:
version "1.0.1"
resolved "https://registry.toot.party/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
@ -1345,6 +1394,11 @@ connect-redis@^4.0.4:
resolved "https://registry.toot.party/connect-redis/-/connect-redis-4.0.4.tgz#b194abe2f3754551f38086e1a28cb9e68d6c3b28"
integrity sha512-aXk7btMlG0J5LqtPNRpFKa5fglzlTzukYNx+Fq8cghbUIQHN/gyK9c3+b0XEROMwiSxMoZDADqjp9tdpUoZLAg==
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.toot.party/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.toot.party/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
@ -1485,6 +1539,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
dependencies:
ms "2.0.0"
debug@^3.2.6:
version "3.2.6"
resolved "https://registry.toot.party/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
dependencies:
ms "^2.1.1"
debug@^4.1.0, debug@^4.1.1:
version "4.1.1"
resolved "https://registry.toot.party/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@ -1544,6 +1605,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.toot.party/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.toot.party/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
denque@^1.4.1:
version "1.4.1"
resolved "https://registry.toot.party/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
@ -1564,6 +1630,11 @@ destroy@~1.0.4:
resolved "https://registry.toot.party/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
detect-libc@^1.0.2:
version "1.0.3"
resolved "https://registry.toot.party/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.toot.party/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
@ -2016,6 +2087,13 @@ fresh@0.5.2:
resolved "https://registry.toot.party/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
fs-minipass@^1.2.5:
version "1.2.7"
resolved "https://registry.toot.party/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
dependencies:
minipass "^2.6.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.toot.party/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@ -2026,6 +2104,20 @@ fsevents@^2.1.2, fsevents@~2.1.2:
resolved "https://registry.toot.party/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.toot.party/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
dependencies:
aproba "^1.0.3"
console-control-strings "^1.0.0"
has-unicode "^2.0.0"
object-assign "^4.1.0"
signal-exit "^3.0.0"
string-width "^1.0.1"
strip-ansi "^3.0.1"
wide-align "^1.1.0"
gensync@^1.0.0-beta.1:
version "1.0.0-beta.1"
resolved "https://registry.toot.party/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
@ -2119,6 +2211,11 @@ has-flag@^4.0.0:
resolved "https://registry.toot.party/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.toot.party/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
has-value@^0.3.1:
version "0.3.1"
resolved "https://registry.toot.party/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@ -2243,13 +2340,20 @@ human-signals@^1.1.1:
resolved "https://registry.toot.party/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
iconv-lite@0.4.24:
iconv-lite@0.4.24, iconv-lite@^0.4.4:
version "0.4.24"
resolved "https://registry.toot.party/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
ignore-walk@^3.0.1:
version "3.0.3"
resolved "https://registry.toot.party/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==
dependencies:
minimatch "^3.0.4"
image-size@^0.7.3:
version "0.7.5"
resolved "https://registry.toot.party/image-size/-/image-size-0.7.5.tgz#269f357cf5797cb44683dfa99790e54c705ead04"
@ -2286,7 +2390,7 @@ inherits@2.0.3:
resolved "https://registry.toot.party/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@^1.3.4:
ini@^1.3.4, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.toot.party/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
@ -2393,6 +2497,13 @@ is-extglob@^2.1.1:
resolved "https://registry.toot.party/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
is-fullwidth-code-point@^1.0.0:
version "1.0.0"
resolved "https://registry.toot.party/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
dependencies:
number-is-nan "^1.0.0"
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.toot.party/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
@ -3303,6 +3414,21 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.toot.party/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
version "2.9.0"
resolved "https://registry.toot.party/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
dependencies:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minizlib@^1.2.1:
version "1.3.3"
resolved "https://registry.toot.party/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
dependencies:
minipass "^2.9.0"
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.toot.party/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
@ -3661,6 +3787,13 @@ mkdirp@1.x, mkdirp@~1.0.3:
resolved "https://registry.toot.party/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mkdirp@^0.5.0, mkdirp@^0.5.1:
version "0.5.5"
resolved "https://registry.toot.party/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "^1.2.5"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.toot.party/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -3708,6 +3841,15 @@ natural-compare@^1.4.0:
resolved "https://registry.toot.party/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
needle@^2.2.1:
version "2.4.1"
resolved "https://registry.toot.party/needle/-/needle-2.4.1.tgz#14af48732463d7475696f937626b1b993247a56a"
integrity sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==
dependencies:
debug "^3.2.6"
iconv-lite "^0.4.4"
sax "^1.2.4"
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.toot.party/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@ -3725,6 +3867,11 @@ no-case@^2.2.0:
dependencies:
lower-case "^1.1.1"
node-addon-api@^2.0.0:
version "2.0.0"
resolved "https://registry.toot.party/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b"
integrity sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.toot.party/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@ -3746,12 +3893,28 @@ node-notifier@^6.0.0:
shellwords "^0.1.1"
which "^1.3.1"
node-pre-gyp@^0.14.0:
version "0.14.0"
resolved "https://registry.toot.party/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83"
integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==
dependencies:
detect-libc "^1.0.2"
mkdirp "^0.5.1"
needle "^2.2.1"
nopt "^4.0.1"
npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.2.7"
rimraf "^2.6.1"
semver "^5.3.0"
tar "^4.4.2"
nodemailer@^6.4.6:
version "6.4.6"
resolved "https://registry.toot.party/nodemailer/-/nodemailer-6.4.6.tgz#d37f504f6560b36616f646a606894fe18819107f"
integrity sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA==
nopt@^4.0.3:
nopt@^4.0.1, nopt@^4.0.3:
version "4.0.3"
resolved "https://registry.toot.party/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
@ -3781,6 +3944,27 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.toot.party/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
npm-bundled@^1.0.1:
version "1.1.1"
resolved "https://registry.toot.party/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b"
integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==
dependencies:
npm-normalize-package-bin "^1.0.1"
npm-normalize-package-bin@^1.0.1:
version "1.0.1"
resolved "https://registry.toot.party/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
npm-packlist@^1.1.6:
version "1.4.8"
resolved "https://registry.toot.party/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
dependencies:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
npm-normalize-package-bin "^1.0.1"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.toot.party/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@ -3795,6 +3979,16 @@ npm-run-path@^4.0.0:
dependencies:
path-key "^3.0.0"
npmlog@^4.0.2:
version "4.1.2"
resolved "https://registry.toot.party/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
dependencies:
are-we-there-yet "~1.1.2"
console-control-strings "~1.1.0"
gauge "~2.7.3"
set-blocking "~2.0.0"
nth-check@~1.0.1:
version "1.0.2"
resolved "https://registry.toot.party/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
@ -3802,6 +3996,11 @@ nth-check@~1.0.1:
dependencies:
boolbase "~1.0.0"
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.toot.party/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
nunjucks@^3.2.1:
version "3.2.1"
resolved "https://registry.toot.party/nunjucks/-/nunjucks-3.2.1.tgz#f229539281e92c6ad25d8c578c9bdb41655caf83"
@ -3823,6 +4022,11 @@ oauth-sign@~0.9.0:
resolved "https://registry.toot.party/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.toot.party/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
object-copy@^0.1.0:
version "0.1.0"
resolved "https://registry.toot.party/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
@ -4138,6 +4342,16 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
rc@^1.2.7:
version "1.2.8"
resolved "https://registry.toot.party/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
dependencies:
deep-extend "^0.6.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-is@^16.12.0:
version "16.13.1"
resolved "https://registry.toot.party/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -4162,7 +4376,7 @@ read-pkg@^5.2.0:
parse-json "^5.0.0"
type-fest "^0.6.0"
readable-stream@2.3.7:
readable-stream@2.3.7, readable-stream@^2.0.6:
version "2.3.7"
resolved "https://registry.toot.party/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@ -4342,6 +4556,13 @@ ret@~0.1.10:
resolved "https://registry.toot.party/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
rimraf@^2.6.1:
version "2.7.1"
resolved "https://registry.toot.party/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
dependencies:
glob "^7.1.3"
rimraf@^3.0.0:
version "3.0.2"
resolved "https://registry.toot.party/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@ -4391,6 +4612,11 @@ sane@^4.0.3:
minimist "^1.1.1"
walker "~1.0.5"
sax@^1.2.4:
version "1.2.4"
resolved "https://registry.toot.party/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
saxes@^3.1.9:
version "3.1.11"
resolved "https://registry.toot.party/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b"
@ -4398,7 +4624,7 @@ saxes@^3.1.9:
dependencies:
xmlchars "^2.1.1"
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.toot.party/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -4437,7 +4663,7 @@ serve-static@1.14.1:
parseurl "~1.3.3"
send "0.17.1"
set-blocking@^2.0.0:
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.toot.party/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
@ -4669,6 +4895,23 @@ string-length@^3.1.0:
astral-regex "^1.0.0"
strip-ansi "^5.2.0"
string-width@^1.0.1:
version "1.0.2"
resolved "https://registry.toot.party/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
dependencies:
code-point-at "^1.0.0"
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2":
version "2.1.1"
resolved "https://registry.toot.party/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
dependencies:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string-width@^3.0.0, string-width@^3.1.0:
version "3.1.0"
resolved "https://registry.toot.party/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
@ -4701,6 +4944,20 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.toot.party/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
dependencies:
ansi-regex "^2.0.0"
strip-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.toot.party/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
dependencies:
ansi-regex "^3.0.0"
strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.toot.party/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
@ -4730,6 +4987,11 @@ strip-final-newline@^2.0.0:
resolved "https://registry.toot.party/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.toot.party/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
supports-color@^5.3.0:
version "5.5.0"
resolved "https://registry.toot.party/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@ -4757,6 +5019,19 @@ symbol-tree@^3.2.2:
resolved "https://registry.toot.party/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
tar@^4.4.2:
version "4.4.13"
resolved "https://registry.toot.party/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
dependencies:
chownr "^1.1.1"
fs-minipass "^1.2.5"
minipass "^2.8.6"
minizlib "^1.2.1"
mkdirp "^0.5.0"
safe-buffer "^5.1.2"
yallist "^3.0.3"
terminal-link@^2.0.0:
version "2.1.1"
resolved "https://registry.toot.party/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
@ -5143,6 +5418,13 @@ which@^2.0.1, which@^2.0.2:
dependencies:
isexe "^2.0.0"
wide-align@^1.1.0:
version "1.1.3"
resolved "https://registry.toot.party/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
dependencies:
string-width "^1.0.2 || 2"
word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.toot.party/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
@ -5211,6 +5493,11 @@ yallist@^2.1.2:
resolved "https://registry.toot.party/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
yallist@^3.0.0, yallist@^3.0.3:
version "3.1.1"
resolved "https://registry.toot.party/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yargs-parser@18.x, yargs-parser@^18.1.1:
version "18.1.3"
resolved "https://registry.toot.party/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"