mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
Merge pull request #199 from matrix-org/gnuxie/remove-axios
Remove axios (from tests)
This commit is contained in:
commit
0cde70e846
@ -16,12 +16,10 @@
|
|||||||
"test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts"
|
"test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/axios": "^0.14.0",
|
|
||||||
"@types/crypto-js": "^4.0.2",
|
"@types/crypto-js": "^4.0.2",
|
||||||
"@types/jsdom": "^16.2.11",
|
"@types/jsdom": "^16.2.11",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
"@types/node": "^16.7.10",
|
"@types/node": "^16.7.10",
|
||||||
"axios": "^0.21.4",
|
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"eslint": "^7.32",
|
"eslint": "^7.32",
|
||||||
"expect": "^27.0.6",
|
"expect": "^27.0.6",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { HmacSHA1 } from "crypto-js";
|
import { HmacSHA1 } from "crypto-js";
|
||||||
import { LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk";
|
import { getRequestFn, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk";
|
||||||
import config from "../../src/config";
|
import config from "../../src/config";
|
||||||
|
|
||||||
const REGISTRATION_ATTEMPTS = 10;
|
const REGISTRATION_ATTEMPTS = 10;
|
||||||
@ -17,24 +16,38 @@ const REGISTRATION_RETRY_BASE_DELAY_MS = 100;
|
|||||||
* @param admin True to make the user an admin, false otherwise.
|
* @param admin True to make the user an admin, false otherwise.
|
||||||
* @returns The response from synapse.
|
* @returns The response from synapse.
|
||||||
*/
|
*/
|
||||||
export async function registerUser(username: string, displayname: string, password: string, admin: boolean) {
|
export async function registerUser(username: string, displayname: string, password: string, admin: boolean): Promise<void> {
|
||||||
const registerUrl = `${config.homeserverUrl}/_synapse/admin/v1/register`;
|
let registerUrl = `${config.homeserverUrl}/_synapse/admin/v1/register`
|
||||||
|
const data: {nonce: string} = await new Promise((resolve, reject) => {
|
||||||
|
getRequestFn()({uri: registerUrl, method: "GET", timeout: 60000}, (error, response, resBody) => {
|
||||||
|
error ? reject(error) : resolve(JSON.parse(resBody))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const nonce = data.nonce!;
|
||||||
|
let mac = HmacSHA1(`${nonce}\0${username}\0${password}\0${admin ? 'admin' : 'notadmin'}`, 'REGISTRATION_SHARED_SECRET');
|
||||||
for (let i = 1; i <= REGISTRATION_ATTEMPTS; ++i) {
|
for (let i = 1; i <= REGISTRATION_ATTEMPTS; ++i) {
|
||||||
try {
|
try {
|
||||||
const { data: { nonce } } = await axios.get(registerUrl);
|
const params = {
|
||||||
const mac = HmacSHA1(`${nonce}\0${username}\0${password}\0${admin ? 'admin' : 'notadmin'}`, 'REGISTRATION_SHARED_SECRET');
|
uri: registerUrl,
|
||||||
return await axios.post(registerUrl, {
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({
|
||||||
nonce,
|
nonce,
|
||||||
username,
|
username,
|
||||||
displayname,
|
displayname,
|
||||||
password,
|
password,
|
||||||
admin,
|
admin,
|
||||||
mac: mac.toString()
|
mac: mac.toString()
|
||||||
|
}),
|
||||||
|
timeout: 60000
|
||||||
|
}
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
getRequestFn()(params, error => error ? reject(error) : resolve());
|
||||||
});
|
});
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
// In case of timeout or throttling, backoff and retry.
|
// In case of timeout or throttling, backoff and retry.
|
||||||
if (ex?.code === 'ESOCKETTIMEDOUT' || ex?.code === 'ETIMEDOUT'
|
if (ex?.code === 'ESOCKETTIMEDOUT' || ex?.code === 'ETIMEDOUT'
|
||||||
|| ex?.response?.data?.errcode === 'M_LIMIT_EXCEEDED') {
|
|| ex?.body?.errcode === 'M_LIMIT_EXCEEDED') {
|
||||||
await new Promise(resolve => setTimeout(resolve, REGISTRATION_RETRY_BASE_DELAY_MS * i * i));
|
await new Promise(resolve => setTimeout(resolve, REGISTRATION_RETRY_BASE_DELAY_MS * i * i));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -80,7 +93,7 @@ async function registerNewTestUser(options: RegistrationOptions) {
|
|||||||
await registerUser(username, username, username, options.isAdmin);
|
await registerUser(username, username, username, options.isAdmin);
|
||||||
return username;
|
return username;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.isAxiosError && e?.response?.data?.errcode === 'M_USER_IN_USE') {
|
if (e?.body?.errcode === 'M_USER_IN_USE') {
|
||||||
if ("exact" in options.name) {
|
if ("exact" in options.name) {
|
||||||
LogService.debug("test/clientHelper", `${username} already registered, reusing`);
|
LogService.debug("test/clientHelper", `${username} already registered, reusing`);
|
||||||
return username;
|
return username;
|
||||||
|
@ -33,7 +33,7 @@ import { getFirstReaction } from "./commandUtils";
|
|||||||
await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"});
|
await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
moderator.start();
|
await moderator.start();
|
||||||
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
||||||
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId} ${targetRoom}` });
|
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId} ${targetRoom}` });
|
||||||
});
|
});
|
||||||
@ -80,7 +80,7 @@ import { getFirstReaction } from "./commandUtils";
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
moderator.start();
|
await moderator.start();
|
||||||
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
||||||
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId}` });
|
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId}` });
|
||||||
});
|
});
|
||||||
@ -116,7 +116,7 @@ import { getFirstReaction } from "./commandUtils";
|
|||||||
let eventToRedact = await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"});
|
let eventToRedact = await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
moderator.start();
|
await moderator.start();
|
||||||
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
||||||
return await moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text', body: `!mjolnir redact https://matrix.to/#/${encodeURIComponent(targetRoom)}/${encodeURIComponent(eventToRedact)}`});
|
return await moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text', body: `!mjolnir redact https://matrix.to/#/${encodeURIComponent(targetRoom)}/${encodeURIComponent(eventToRedact)}`});
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,8 @@ export const mochaHooks = {
|
|||||||
beforeEach: [
|
beforeEach: [
|
||||||
async function() {
|
async function() {
|
||||||
console.log("mochaHooks.beforeEach");
|
console.log("mochaHooks.beforeEach");
|
||||||
|
// Sometimes it takes a little longer to register users.
|
||||||
|
this.timeout(3000)
|
||||||
this.managementRoomAlias = config.managementRoom;
|
this.managementRoomAlias = config.managementRoom;
|
||||||
this.mjolnir = await makeMjolnir();
|
this.mjolnir = await makeMjolnir();
|
||||||
config.RUNTIME.client = this.mjolnir.client;
|
config.RUNTIME.client = this.mjolnir.client;
|
||||||
|
@ -52,13 +52,10 @@ async function configureMjolnir() {
|
|||||||
try {
|
try {
|
||||||
await registerUser(config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true)
|
await registerUser(config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.isAxiosError) {
|
if (e?.body?.errcode === 'M_USER_IN_USE') {
|
||||||
console.log('Received error while registering', e.response.data || e.response);
|
|
||||||
if (e.response.data && e.response.data.errcode === 'M_USER_IN_USE') {
|
|
||||||
console.log(`${config.pantalaimon.username} already registered, skipping`);
|
console.log(`${config.pantalaimon.username} already registered, skipping`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
throw e;
|
throw e;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
import { strict as assert } from "assert";
|
import { strict as assert } from "assert";
|
||||||
|
|
||||||
import { newTestUser } from "./clientHelper";
|
import { newTestUser } from "./clientHelper";
|
||||||
import { getMessagesByUserIn } from "../../src/utils";
|
|
||||||
import config from "../../src/config";
|
import config from "../../src/config";
|
||||||
import axios from "axios";
|
import { getRequestFn, LogService, MatrixClient } from "matrix-bot-sdk";
|
||||||
import { LogService } from "matrix-bot-sdk";
|
|
||||||
import { createBanList, getFirstReaction } from "./commands/commandUtils";
|
import { createBanList, getFirstReaction } from "./commands/commandUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a copy of the rules from the ruleserver.
|
* Get a copy of the rules from the ruleserver.
|
||||||
*/
|
*/
|
||||||
async function currentRules() {
|
async function currentRules(): Promise<{ start: object, stop: object, since: string }> {
|
||||||
return await (await axios.get(`http://${config.web.address}:${config.web.port}/api/1/ruleserver/updates/`)).data
|
return await new Promise((resolve, reject) => getRequestFn()({
|
||||||
|
uri: `http://${config.web.address}:${config.web.port}/api/1/ruleserver/updates/`,
|
||||||
|
method: "GET"
|
||||||
|
}, (error, response, body) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
} else {
|
||||||
|
resolve(JSON.parse(body))
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,11 +70,11 @@ describe("Test: that policy lists are consumed by the associated synapse module"
|
|||||||
it('blocks users in antispam when they are banned from sending messages and invites serverwide.', async function() {
|
it('blocks users in antispam when they are banned from sending messages and invites serverwide.', async function() {
|
||||||
this.timeout(20000);
|
this.timeout(20000);
|
||||||
// Create a few users and a room.
|
// Create a few users and a room.
|
||||||
let badUser = await newTestUser(false, "spammer");
|
let badUser = await newTestUser({ name: { contains: "spammer" }});
|
||||||
let badUserId = await badUser.getUserId();
|
let badUserId = await badUser.getUserId();
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
let mjolnirUserId = await mjolnir.getUserId();
|
let mjolnirUserId = await mjolnir.getUserId();
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
this.moderator = moderator;
|
this.moderator = moderator;
|
||||||
await moderator.joinRoom(this.mjolnir.managementRoomId);
|
await moderator.joinRoom(this.mjolnir.managementRoomId);
|
||||||
let unprotectedRoom = await badUser.createRoom({ invite: [await moderator.getUserId()]});
|
let unprotectedRoom = await badUser.createRoom({ invite: [await moderator.getUserId()]});
|
||||||
@ -98,9 +105,9 @@ describe("Test: that policy lists are consumed by the associated synapse module"
|
|||||||
})
|
})
|
||||||
it('Test: Cannot send message to a room that is listed in a policy list and cannot invite a user to the room either', async function () {
|
it('Test: Cannot send message to a room that is listed in a policy list and cannot invite a user to the room either', async function () {
|
||||||
this.timeout(20000);
|
this.timeout(20000);
|
||||||
let badUser = await newTestUser(false, "spammer");
|
let badUser = await newTestUser({ name: { contains: "spammer" }});
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
await moderator.joinRoom(this.mjolnir.managementRoomId);
|
await moderator.joinRoom(this.mjolnir.managementRoomId);
|
||||||
const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator);
|
const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator);
|
||||||
let badRoom = await badUser.createRoom();
|
let badRoom = await badUser.createRoom();
|
||||||
@ -128,7 +135,7 @@ describe("Test: that policy lists are consumed by the associated synapse module"
|
|||||||
it('Test: When a list becomes unwatched, the associated policies are stopped.', async function () {
|
it('Test: When a list becomes unwatched, the associated policies are stopped.', async function () {
|
||||||
this.timeout(20000);
|
this.timeout(20000);
|
||||||
const mjolnir = config.RUNTIME.client!
|
const mjolnir = config.RUNTIME.client!
|
||||||
let moderator = await newTestUser(false, "moderator");
|
let moderator = await newTestUser({ name: { contains: "moderator" }});
|
||||||
await moderator.joinRoom(this.mjolnir.managementRoomId);
|
await moderator.joinRoom(this.mjolnir.managementRoomId);
|
||||||
const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator);
|
const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator);
|
||||||
let targetRoom = await moderator.createRoom();
|
let targetRoom = await moderator.createRoom();
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { strict as assert } from "assert";
|
import { strict as assert } from "assert";
|
||||||
|
|
||||||
import config from "../../src/config";
|
import config from "../../src/config";
|
||||||
|
import { Mjolnir } from "../../src/Mjolnir";
|
||||||
|
import { IProtection } from "../../src/protections/IProtection";
|
||||||
import { PROTECTIONS } from "../../src/protections/protections";
|
import { PROTECTIONS } from "../../src/protections/protections";
|
||||||
import { ProtectionSettingValidationError } from "../../src/protections/ProtectionSettings";
|
import { ProtectionSettingValidationError } from "../../src/protections/ProtectionSettings";
|
||||||
import { NumberProtectionSetting, StringProtectionSetting, StringListProtectionSetting } from "../../src/protections/ProtectionSettings";
|
import { NumberProtectionSetting, StringProtectionSetting, StringListProtectionSetting } from "../../src/protections/ProtectionSettings";
|
||||||
@ -10,7 +12,7 @@ import { matrixClient, mjolnir } from "./mjolnirSetupUtils";
|
|||||||
describe("Test: Protection settings", function() {
|
describe("Test: Protection settings", function() {
|
||||||
let client;
|
let client;
|
||||||
this.beforeEach(async function () {
|
this.beforeEach(async function () {
|
||||||
client = await newTestUser(true);
|
client = await newTestUser({ name: { contains: "protection-settings" }});
|
||||||
await client.start();
|
await client.start();
|
||||||
})
|
})
|
||||||
this.afterEach(async function () {
|
this.afterEach(async function () {
|
||||||
|
@ -50,7 +50,7 @@ describe("Test: Mjolnir can still sync and respond to commands while throttled",
|
|||||||
await Promise.all([...Array(50).keys()].map((i) => badUser.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Bad Message #${i}`})));
|
await Promise.all([...Array(50).keys()].map((i) => badUser.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Bad Message #${i}`})));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
moderator.start();
|
await moderator.start();
|
||||||
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => {
|
||||||
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId} ${targetRoom}` });
|
return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId} ${targetRoom}` });
|
||||||
});
|
});
|
||||||
|
19
yarn.lock
19
yarn.lock
@ -83,13 +83,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||||
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
||||||
|
|
||||||
"@types/axios@^0.14.0":
|
|
||||||
version "0.14.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"
|
|
||||||
integrity sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=
|
|
||||||
dependencies:
|
|
||||||
axios "*"
|
|
||||||
|
|
||||||
"@types/body-parser@*":
|
"@types/body-parser@*":
|
||||||
version "1.19.1"
|
version "1.19.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c"
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c"
|
||||||
@ -386,13 +379,6 @@ aws4@^1.8.0:
|
|||||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
||||||
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
||||||
|
|
||||||
axios@*, axios@^0.21.4:
|
|
||||||
version "0.21.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
|
|
||||||
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
|
|
||||||
dependencies:
|
|
||||||
follow-redirects "^1.14.0"
|
|
||||||
|
|
||||||
balanced-match@^1.0.0:
|
balanced-match@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
@ -1123,11 +1109,6 @@ flatted@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
|
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
|
||||||
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
|
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
|
||||||
|
|
||||||
follow-redirects@^1.14.0:
|
|
||||||
version "1.14.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
|
|
||||||
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
|
|
||||||
|
|
||||||
forever-agent@~0.6.1:
|
forever-agent@~0.6.1:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||||
|
Loading…
Reference in New Issue
Block a user