Add support for listening to github notifications

This commit is contained in:
Half-Shot 2020-02-18 17:16:21 +00:00
parent 8de70a0035
commit a8500c107f
10 changed files with 431 additions and 29 deletions

View File

@ -16,6 +16,8 @@
"lint": "tslint -p tsconfig.json"
},
"dependencies": {
"@octokit/auth-app": "^2.4.2",
"@octokit/auth-token": "^2.4.0",
"@octokit/rest": "^16.43.1",
"axios": "^0.19.2",
"express": "^4.17.1",

View File

@ -7,39 +7,74 @@ import uuid from "uuid/v4";
import qs from "querystring";
export const BRIDGE_ROOM_TYPE = "uk.half-shot.matrix-github.room";
export const BRIDGE_NOTIF_TYPE = "uk.half-shot.matrix-github.notif_state";
export interface AdminAccountData {
admin_user: string;
notifications?: {
enabled: boolean;
}
}
export class AdminRoom {
private pendingOAuthState: string|null = null;
constructor(private roomId: string,
public readonly userId: string,
public readonly data: AdminAccountData,
private botIntent: Intent,
private tokenStore: UserTokenStore,
private config: BridgeConfig) {
}
public get userId() {
return this.data.admin_user;
}
public get oauthState() {
return this.pendingOAuthState;
}
public get notificationsEnabled() {
return this.data.notifications?.enabled;
}
public clearOauthState() {
this.pendingOAuthState = null;
}
public async getNotifSince() {
try {
const { since } = await this.botIntent.underlyingClient.getRoomAccountData(BRIDGE_NOTIF_TYPE, this.roomId);
console.log(`getNotifSince for ${this.roomId} = ${since}`);
return since;
} catch {
// TODO: We should look at this error.
return 0;
}
}
public async setNotifSince(since: number) {
console.log(`setNotifSince for ${this.roomId} = ${since}`);
return this.botIntent.underlyingClient.setRoomAccountData(BRIDGE_NOTIF_TYPE, this.roomId, {
since,
});
}
public async handleCommand(command: string) {
const cmdLower = command.toLowerCase();
if (cmdLower.startsWith("!setpersonaltoken ")) {
const accessToken = command.substr("!setPersonalToken ".length);
await this.setPersonalAccessToken(accessToken);
return;
} else if (cmdLower.startsWith("!hastoken")) {
await this.hasPersonalToken();
return;
} else if (cmdLower.startsWith("!startoauth")) {
await this.beginOAuth();
return;
if (cmdLower.startsWith("setpersonaltoken ")) {
const accessToken = command.substr("setPersonalToken ".length);
return this.setPersonalAccessToken(accessToken);
} else if (cmdLower.startsWith("hastoken")) {
return this.hasPersonalToken();
} else if (cmdLower.startsWith("startoauth")) {
return this.beginOAuth();
} else if (cmdLower.startsWith("notifications enable")) {
// TODO: Check if we can do this.
return this.setNotificationsState(true);
} else if (cmdLower.startsWith("notifications disable")) {
return this.setNotificationsState(false);
}
await this.sendNotice("Command not understood");
}
@ -82,9 +117,17 @@ export class AdminRoom {
await this.sendNotice(`You should follow ${url} to link your account to the bridge`);
}
private async setNotificationsState(enabled: boolean) {
const data: AdminAccountData = await this.botIntent.underlyingClient.getRoomAccountData(BRIDGE_ROOM_TYPE, this.roomId);
if (data.notifications?.enabled === enabled) {
return this.sendNotice(`Notifications are already ${enabled ? "en" : "dis"}abled`);
}
data.notifications = { enabled };
await this.botIntent.underlyingClient.setRoomAccountData(BRIDGE_ROOM_TYPE, this.roomId, data);
return this.sendNotice(`${enabled ? "En" : "Dis"}abled GitHub notifcations`);
}
private async sendNotice(noticeText: string) {
return this.botIntent.sendText(this.roomId, noticeText, "m.notice");
}
// Initiate oauth
// Relinquish oauth
}

View File

@ -1,4 +1,8 @@
import { Octokit } from '@octokit/rest';
import { UserNotification } from './UserNotificationWatcher';
import markdown from "markdown-it";
const md = new markdown();
export class FormatUtil {
public static formatName(issue: Octokit.IssuesGetResponse) {
@ -9,4 +13,30 @@ export class FormatUtil {
public static formatTopic(issue: Octokit.IssuesGetResponse) {
return `${issue.title} | Status: ${issue.state} | ${issue.html_url}`;
}
public static formatNotification(notif: UserNotification): {plain: string, html: string} {
let plain = `${this.getEmojiForNotifType(notif)} [${notif.subject.title}](${notif.subject.url_data.html_url})`;
if (notif.repository) {
plain += ` for **[${notif.repository.full_name}](${notif.repository.html_url})**`;
}
const commentData = notif.subject.latest_comment_url_data;
if (commentData && commentData.body) {
plain += `\n\n**@${commentData.user.login}**: ${commentData.body}`;
}
return {
plain,
html: md.render(plain),
}
}
private static getEmojiForNotifType(notif: UserNotification): string {
switch(notif.subject.type) {
case "Issue":
return "📝";
case "PullRequest":
return "✋"; // What should we do about this?
default:
return "🔔";
}
}
}

View File

@ -5,7 +5,7 @@ import { createAppAuth } from "@octokit/auth-app";
import markdown from "markdown-it";
import { IBridgeRoomState, BRIDGE_STATE_TYPE } from "./BridgeState";
import { BridgeConfig } from "./Config";
import { IWebhookEvent, IOAuthRequest, IOAuthTokens } from "./GithubWebhooks";
import { IWebhookEvent, IOAuthRequest, IOAuthTokens, NotificationsEnableEvent } from "./GithubWebhooks";
import { CommentProcessor } from "./CommentProcessor";
import { MessageQueue, createMessageQueue } from "./MessageQueue/MessageQueue";
import { AdminRoom, BRIDGE_ROOM_TYPE } from "./AdminRoom";
@ -14,7 +14,8 @@ import { FormatUtil } from "./FormatUtil";
import { MatrixEvent, MatrixMemberContent, MatrixMessageContent, MatrixEventContent } from "./MatrixEvent";
import { LogWrapper } from "./LogWrapper";
import { IMatrixSendMessage, IMatrixSendMessageResponse } from "./MatrixSender";
import { promises as fs} from "fs";
import { promises as fs } from "fs";
import { UserNotificationsEvent, UserNotification } from "./UserNotificationWatcher";
const md = new markdown();
const log = new LogWrapper("GithubBridge");
@ -80,6 +81,7 @@ export class GithubBridge {
this.queue.subscribe("comment.*");
this.queue.subscribe("issue.*");
this.queue.subscribe("response.matrix.message");
this.queue.subscribe("notifications.user.events");
this.queue.on<IWebhookEvent>("comment.created", async (msg) => {
return this.onCommentCreated(msg.data);
@ -97,6 +99,26 @@ export class GithubBridge {
return this.onIssueStateChange(msg.data);
});
this.queue.on<UserNotificationsEvent>("notifications.user.events", async (msg) => {
const adminRoom = this.adminRooms.get(msg.data.roomId);
if (!adminRoom) {
log.warn("No admin room for this notif stream!");
return;
}
for (const event of msg.data.events) {
try {
await this.handleUserNotification(msg.data.roomId, event);
} catch (ex) {
log.warn("Failed to handle event:", ex);
}
}
try {
await adminRoom.setNotifSince(msg.data.lastReadTs);
} catch (ex) {
log.error("Failed to update stream position for notifications:", ex);
}
});
this.queue.on<IOAuthRequest>("oauth.response", async (msg) => {
const adminRoom = [...this.adminRooms.values()].find((r) => r.oauthState === msg.data.state);
this.queue.push<boolean>({
@ -126,9 +148,27 @@ export class GithubBridge {
BRIDGE_ROOM_TYPE, roomId,
);
if (accountData.type === "admin") {
this.adminRooms.set(roomId, new AdminRoom(
roomId, accountData.admin_user, this.as.botIntent, this.tokenStore, this.config,
));
const adminRoom = new AdminRoom(
roomId, accountData, this.as.botIntent, this.tokenStore, this.config,
);
this.adminRooms.set(roomId, adminRoom);
log.info(`${roomId} is an admin room for ${adminRoom.userId}`);
if (adminRoom.notificationsEnabled) {
log.info(`Notifications enabled for ${adminRoom.userId}`);
const token = await this.tokenStore.getUserToken(adminRoom.userId);
console.log(token);
if (token) {
log.info(`Notifications enabled for ${adminRoom.userId} and token was found`);
this.queue.push<NotificationsEnableEvent>({ eventName: "notifications.user.enable", sender: "GithubBridge", data: {
user_id: adminRoom.userId,
room_id: roomId,
token,
since: await adminRoom.getNotifSince(),
}});
} else {
log.warn(`Notifications enabled for ${adminRoom.userId} but no token stored!`);
}
}
}
continue;
} catch (ex) { /* this is an old style room */ }
@ -189,12 +229,13 @@ export class GithubBridge {
await this.as.botIntent.underlyingClient.leaveRoom(roomId);
return;
}
const data = {admin_user: event.sender, type: "admin"};
await this.as.botIntent.underlyingClient.setRoomAccountData(
BRIDGE_ROOM_TYPE, roomId, {admin_user: event.sender, type: "admin"},
BRIDGE_ROOM_TYPE, roomId, data,
);
this.adminRooms.set(
roomId,
new AdminRoom(roomId, event.sender, this.as.botIntent, this.tokenStore, this.config),
new AdminRoom(roomId, data, this.as.botIntent, this.tokenStore, this.config),
);
}
@ -488,6 +529,17 @@ export class GithubBridge {
this.matrixHandledEvents.add(key);
}
private async handleUserNotification(roomId: string, notif: UserNotification) {
log.info("New notification event:", notif.subject);
const formatted = FormatUtil.formatNotification(notif);
this.sendMatrixMessage(roomId, {
msgtype: "m.text",
body: formatted.plain,
formatted_body: formatted.html,
format: "org.matrix.custom.html",
});
}
private async sendMatrixText(roomId: string, text: string, msgtype: string = "m.text",
sender: string|null = null): Promise<string> {
return this.sendMatrixMessage(roomId, {

View File

@ -3,11 +3,12 @@ import { Application, default as express, Request, Response } from "express";
import { createHmac } from "crypto";
import { Octokit } from "@octokit/rest";
import { EventEmitter } from "events";
import { MessageQueue, createMessageQueue } from "./MessageQueue/MessageQueue";
import { MessageQueue, createMessageQueue, MessageQueueMessage } from "./MessageQueue/MessageQueue";
import { LogWrapper } from "./LogWrapper";
import qs from "querystring";
import { Server } from "http";
import axios from "axios";
import { UserNotificationWatcher } from "./UserNotificationWatcher";
const log = new LogWrapper("GithubWebhooks");
@ -35,9 +36,21 @@ export interface IOAuthTokens {
state: string;
}
export interface NotificationsEnableEvent {
user_id: string;
room_id: string;
since: number;
token: string;
}
export interface NotificationsDisableEvent {
user_id: string;
}
export class GithubWebhooks extends EventEmitter {
private expressApp: Application;
private queue: MessageQueue;
private userNotificationWatcher: UserNotificationWatcher;
private server?: Server;
constructor(private config: BridgeConfig) {
super();
@ -48,6 +61,14 @@ export class GithubWebhooks extends EventEmitter {
this.expressApp.post("/", this.onPayload.bind(this));
this.expressApp.get("/oauth", this.onGetOauth.bind(this));
this.queue = createMessageQueue(config);
this.userNotificationWatcher = new UserNotificationWatcher(this.queue);
this.queue.subscribe("notifications.user.*");
this.queue.on("notifications.user.enable", (msg: MessageQueueMessage<NotificationsEnableEvent>) => {
this.userNotificationWatcher.addUser(msg.data);
});
this.queue.on("notifications.user.disable", (msg: MessageQueueMessage<NotificationsDisableEvent>) => {
this.userNotificationWatcher.removeUser(msg.data.user_id);
});
}
public listen() {
@ -55,6 +76,7 @@ export class GithubWebhooks extends EventEmitter {
this.config.github.webhook.port,
this.config.github.webhook.bindAddress,
);
this.userNotificationWatcher.start();
}
public stop() {

View File

@ -44,6 +44,10 @@ export class LogWrapper {
log.warn(getMessageString(messageOrObject), { module });
},
error: (module: string, ...messageOrObject: any[]) => {
if (messageOrObject[0]?.error === "Room account data not found") {
log.debug(getMessageString(messageOrObject), { module });
return; // This is just noise :|
}
log.error(getMessageString(messageOrObject), { module });
},
debug: (module: string, ...messageOrObject: any[]) => {

View File

@ -0,0 +1,131 @@
import { NotificationsEnableEvent } from "./GithubWebhooks";
import { Octokit } from "@octokit/rest";
import { createTokenAuth } from "@octokit/auth-token";
import { LogWrapper } from "./LogWrapper";
import { MessageQueue } from "./MessageQueue/MessageQueue";
interface UserStream {
octoKit: Octokit,
userId: string,
roomId: string,
lastReadTs: number,
}
export interface UserNotificationsEvent {
roomId: string,
lastReadTs: number,
events: UserNotification[],
}
export interface UserNotification {
reason: "assign"|"author"|"comment"|"invitation"|"manual"|"mention"|"review_required"|"security_alert"|"state_change"|"subscribed"|"team_mention";
unread: boolean;
updated_at: number;
last_read_at: number;
url: string;
subject: {
title: string;
url: string;
latest_comment_url: string|null;
type: "PullRequest"|"Issue";
url_data: any;
latest_comment_url_data: any;
};
repository: Octokit.ActivityGetThreadResponseRepository;
}
const MIN_INTERVAL_MS = 45000;
const log = new LogWrapper("UserNotificationWatcher");
export class UserNotificationWatcher {
private userStreams: Map<string, UserStream> = new Map();
private userQueue: string[] = [];
private shouldListen: boolean = false;
constructor(private queue: MessageQueue) {
}
public start() {
this.shouldListen = true;
this.fetchUserNotifications().catch((ex) => {
log.error("CRITICAL ERROR when fethcing user notifications:", ex);
})
}
public async fetchUserNotifications() {
let userId;
while (this.shouldListen) {
userId = this.userQueue.pop();
if (!userId) {
log.info(`No users queued for notifications, waiting for 5s`);
await new Promise((res) => setTimeout(res, 5000));
continue;
}
const stream = this.userStreams.get(userId);
if (!stream) {
log.warn("User is in the userQueue but has no stream, dropping from queue");
continue;
}
const interval = MIN_INTERVAL_MS - (Date.now() - stream.lastReadTs);
if (interval > 0) {
log.info(`We read this users notifications ${MIN_INTERVAL_MS - interval}ms ago, waiting ${interval}ms`);
await new Promise((res) => setTimeout(res, interval));
}
log.info(`Getting notifications for ${userId} ${stream.lastReadTs}`);
try {
const since = stream.lastReadTs !== 0 ? `?since=${new Date(stream.lastReadTs).toISOString()}`: "";
const response = await stream.octoKit.request(`/notifications${since}`);
stream.lastReadTs = Date.now();
const events: UserNotification[] = await Promise.all(response.data.map(async (event: UserNotification) => {
if (event.subject.url) {
const res = await stream.octoKit.request(event.subject.url);
event.subject.url_data = res.data;
}
if (event.subject.latest_comment_url) {
const res = await stream.octoKit.request(event.subject.latest_comment_url);
event.subject.latest_comment_url_data = res.data;
}
return event;
}));
this.queue.push<UserNotificationsEvent>({
eventName: "notifications.user.events",
data: {
roomId: stream.roomId,
events,
lastReadTs: stream.lastReadTs,
},
sender: "GithubWebhooks",
});
} catch (ex) {
log.error("An error occured getting notifications:", ex);
}
this.userQueue.push(userId);
}
}
removeUser(userId: string) {
this.userStreams.delete(userId);
log.info(`Removed ${userId} to notif queue`);
}
addUser(data: NotificationsEnableEvent) {
const clientKit = new Octokit({
authStrategy: createTokenAuth,
auth: data.token,
userAgent: "matrix-github v0.0.1",
});
this.userStreams.set(data.user_id, {
octoKit: clientKit,
userId: data.user_id,
roomId: data.room_id,
lastReadTs: data.since,
});
this.userQueue.push(data.user_id);
log.info(`Added ${data.user_id} to notif queue`);
}
}

View File

@ -32,13 +32,14 @@ export class UserTokenStore {
let obj;
try {
obj = await this.intent.underlyingClient.getAccountData(`${ACCOUNT_DATA_TYPE}${userId}`);
const encryptedTextB64 = obj.encrypted;
const encryptedText = Buffer.from(encryptedTextB64, "base64");
const token = privateDecrypt(this.key, encryptedText).toString("utf-8");
this.userTokens.set(userId, token);
return token;
} catch (ex) {
return null;
log.error("Failed to get token:", ex);
}
const encryptedTextB64 = obj.encrypted;
const encryptedText = Buffer.from(encryptedTextB64, "base64");
const token = privateDecrypt(this.key, encryptedText).toString("utf-8");
this.userTokens.set(userId, token);
return token;
return null;
}
}

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"incremental": true,
"target": "ESNext",
"target": "ES2019",
"module": "commonjs",
"allowJs": false,
"declaration": false,

119
yarn.lock
View File

@ -25,6 +25,19 @@
dependencies:
regenerator-runtime "^0.13.2"
"@octokit/auth-app@^2.4.2":
version "2.4.2"
resolved "https://registry.yarnpkg.com/@octokit/auth-app/-/auth-app-2.4.2.tgz#d6de830f2b0401b9fece6b822361e7d9f010b030"
integrity sha512-t/kCnyCNOySTKmdEjDbG6C/mXKwsBRgbQIxhGA8JsYHDWzvOacQEvWKxWSw/4lR707ZcgPz8BpY0DHDuT6MeVA==
dependencies:
"@octokit/request" "^5.3.0"
"@octokit/request-error" "^1.1.0"
"@octokit/types" "^2.0.0"
"@types/lru-cache" "^5.1.0"
lru-cache "^5.1.1"
universal-github-app-jwt "^1.0.1"
universal-user-agent "^4.0.0"
"@octokit/auth-token@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.0.tgz#b64178975218b99e4dfe948253f0673cbbb59d9f"
@ -69,7 +82,16 @@
deprecation "^2.0.0"
once "^1.4.0"
"@octokit/request@^5.2.0":
"@octokit/request-error@^1.1.0":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801"
integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==
dependencies:
"@octokit/types" "^2.0.0"
deprecation "^2.0.0"
once "^1.4.0"
"@octokit/request@^5.2.0", "@octokit/request@^5.3.0":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.3.1.tgz#3a1ace45e6f88b1be4749c5da963b3a3b4a2f120"
integrity sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==
@ -175,11 +197,23 @@
dependencies:
"@types/node" "*"
"@types/jsonwebtoken@^8.3.3":
version "8.3.7"
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.3.7.tgz#ab79ad55b9435834d24cca3112f42c08eedb1a54"
integrity sha512-B5SSifLkjB0ns7VXpOOtOUlynE78/hKcY8G8pOAhkLJZinwofIBYqz555nRj2W9iDWZqFhK5R+7NZDaRmKWAoQ==
dependencies:
"@types/node" "*"
"@types/linkify-it@*":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
"@types/lru-cache@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03"
integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==
"@types/markdown-it@^0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.8.tgz#9af8704acde87fec70475369ba0413d50717bd8d"
@ -440,6 +474,11 @@ btoa-lite@^1.0.0:
resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337"
integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc=
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@ -770,6 +809,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -1359,6 +1405,22 @@ json-stringify-safe@~5.0.1:
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@ -1369,6 +1431,23 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
kuler@1.0.x:
version "1.0.1"
resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6"
@ -1423,6 +1502,26 @@ lodash.get@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
@ -1438,6 +1537,11 @@ lodash.mergewith@^4.6.1:
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
@ -2099,6 +2203,11 @@ semver@^5.3.0, semver@^5.5.0, semver@^5.7.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@ -2444,6 +2553,14 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
universal-github-app-jwt@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/universal-github-app-jwt/-/universal-github-app-jwt-1.0.1.tgz#d2ecc00249e02c8a244c5b4ab285de8e6bbef1ee"
integrity sha512-neZ16w2WQbXUTuEa3V52fb+Zp84Fth3Yj7hMn0JSky9pgjM874vooDAU58TZLg2ZjxUPig1C80W1jRTaNek4Sw==
dependencies:
"@types/jsonwebtoken" "^8.3.3"
jsonwebtoken "^8.5.1"
universal-user-agent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.0.tgz#27da2ec87e32769619f68a14996465ea1cb9df16"