backend: implement not forwarding block activities (#212)

Fixes FoundKeyGang/FoundKey#211

Commits pulled from https://github.com/misskey-dev/misskey/pull/7799

Changelog: Added
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Co-authored-by: Johann150 <johann.galle@protonmail.com>
Co-authored-by: Francis Dinh <normandy@biribiri.dev>
Reviewed-on: FoundKeyGang/FoundKey#212
This commit is contained in:
Norm 2022-11-17 21:24:38 +00:00
parent 110c645a97
commit 71b3b5a60c
11 changed files with 213 additions and 19 deletions

View file

@ -799,6 +799,8 @@ onlineStatus: "Online status"
hideOnlineStatus: "Hide online status" hideOnlineStatus: "Hide online status"
hideOnlineStatusDescription: "Hiding your online status reduces the convenience of\ hideOnlineStatusDescription: "Hiding your online status reduces the convenience of\
\ some features such as the search." \ some features such as the search."
federateBlocks: "Federate blocks"
federateBlocksDescription: "If disabled, block activities won't be sent."
online: "Online" online: "Online"
active: "Active" active: "Active"
offline: "Offline" offline: "Offline"

View file

@ -740,6 +740,8 @@ unknown: "不明"
onlineStatus: "オンライン状態" onlineStatus: "オンライン状態"
hideOnlineStatus: "オンライン状態を隠す" hideOnlineStatus: "オンライン状態を隠す"
hideOnlineStatusDescription: "オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。" hideOnlineStatusDescription: "オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。"
federateBlocks: "ブロックを連合に送信"
federateBlocksDescription: "オフにするとBlockのActivityは連合に送信しません"
online: "オンライン" online: "オンライン"
active: "アクティブ" active: "アクティブ"
offline: "オフライン" offline: "オフライン"

View file

@ -0,0 +1,12 @@
export class userBlockFederation1631880003000 {
name = 'userBlockFederation1631880003000';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "federateBlocks" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "federateBlocks"`);
}
}

View file

@ -158,6 +158,7 @@
"@types/sanitize-html": "2.6.2", "@types/sanitize-html": "2.6.2",
"@types/semver": "7.3.12", "@types/semver": "7.3.12",
"@types/sharp": "0.30.5", "@types/sharp": "0.30.5",
"@types/sinon": "^10.0.13",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7", "@types/speakeasy": "2.0.7",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
@ -173,6 +174,7 @@
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"execa": "6.1.0", "execa": "6.1.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"sinon": "^14.0.2",
"typescript": "^4.8.3" "typescript": "^4.8.3"
} }
} }

View file

@ -218,6 +218,11 @@ export class User {
}) })
public token: string | null; public token: string | null;
@Column('boolean', {
default: true,
})
public federateBlocks: boolean;
constructor(data: Partial<User>) { constructor(data: Partial<User>) {
if (data == null) return; if (data == null) return;

View file

@ -393,6 +393,7 @@ export const UserRepository = db.getRepository(User).extend({
mutingNotificationTypes: profile!.mutingNotificationTypes, mutingNotificationTypes: profile!.mutingNotificationTypes,
emailNotificationTypes: profile!.emailNotificationTypes, emailNotificationTypes: profile!.emailNotificationTypes,
showTimelineReplies: user.showTimelineReplies || falsy, showTimelineReplies: user.showTimelineReplies || falsy,
federateBlocks: user!.federateBlocks,
} : {}), } : {}),
...(opts.includeSecrets ? { ...(opts.includeSecrets ? {

View file

@ -81,6 +81,7 @@ export const paramDef = {
emailNotificationTypes: { type: 'array', items: { emailNotificationTypes: { type: 'array', items: {
type: 'string', type: 'string',
} }, } },
federateBlocks: { type: 'boolean' },
}, },
} as const; } as const;
@ -129,6 +130,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.federateBlocks === 'boolean') updates.federateBlocks = ps.federateBlocks;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;

View file

@ -12,7 +12,7 @@ import { perUserFollowingChart } from '@/services/chart/index.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
import { getActiveWebhooks } from '@/misc/webhook-cache.js'; import { getActiveWebhooks } from '@/misc/webhook-cache.js';
export default async function(blocker: User, blockee: User) { export default async function(blocker: User, blockee: User): Promise<void> {
await Promise.all([ await Promise.all([
cancelRequest(blocker, blockee), cancelRequest(blocker, blockee),
cancelRequest(blockee, blocker), cancelRequest(blockee, blocker),
@ -32,13 +32,13 @@ export default async function(blocker: User, blockee: User) {
await Blockings.insert(blocking); await Blockings.insert(blocking);
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee) && blocker.federateBlocks) {
const content = renderActivity(renderBlock(blocking)); const content = renderActivity(renderBlock(blocking));
deliver(blocker, content, blockee.inbox); deliver(blocker, content, blockee.inbox);
} }
} }
async function cancelRequest(follower: User, followee: User) { async function cancelRequest(follower: User, followee: User): Promise<void> {
const request = await FollowRequests.findOneBy({ const request = await FollowRequests.findOneBy({
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
@ -75,20 +75,20 @@ async function cancelRequest(follower: User, followee: User) {
}); });
} }
// リモートにフォローリクエストをしていたらUndoFollow送信 // Send Undo Follow if followee is remote
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox); deliver(follower, content, followee.inbox);
} }
// リモートからフォローリクエストを受けていたらReject送信 // Send Reject if follower is remote
if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId!), followee)); const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId!), followee));
deliver(followee, content, follower.inbox); deliver(followee, content, follower.inbox);
} }
} }
async function unFollow(follower: User, followee: User) { async function unFollow(follower: User, followee: User): Promise<void> {
const following = await Followings.findOneBy({ const following = await Followings.findOneBy({
followerId: follower.id, followerId: follower.id,
followeeId: followee.id, followeeId: followee.id,
@ -122,14 +122,14 @@ async function unFollow(follower: User, followee: User) {
}); });
} }
// リモートにフォローをしていたらUndoFollow送信 // Send Undo Follow if follower is remote
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox); deliver(follower, content, followee.inbox);
} }
} }
async function removeFromList(listOwner: User, user: User) { async function removeFromList(listOwner: User, user: User): Promise<void> {
const userLists = await UserLists.findBy({ const userLists = await UserLists.findBy({
userId: listOwner.id, userId: listOwner.id,
}); });

View file

@ -0,0 +1,58 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import * as sinon from 'sinon';
import { async, signup, startServer, shutdownServer, initTestDb } from '../utils.js';
describe('Creating a block activity', () => {
let p: childProcess.ChildProcess;
// alice blocks bob
let alice: any;
let bob: any;
let carol: any;
before(async () => {
await initTestDb();
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
bob.host = 'http://remote';
carol.host = 'http://remote';
});
beforeEach(() => {
sinon.restore();
});
after(async () => {
await shutdownServer(p);
});
it('Should federate blocks normally', async(async () => {
const createBlock = (await import('../../src/services/blocking/create')).default;
const deleteBlock = (await import('../../src/services/blocking/delete')).default;
const queues = await import('../../src/queue/index');
const spy = sinon.spy(queues, 'deliver');
await createBlock(alice, bob);
assert(spy.calledOnce);
await deleteBlock(alice, bob);
assert(spy.calledTwice);
}));
it('Should not federate blocks if federateBlocks is false', async () => {
const createBlock = (await import('../../src/services/blocking/create')).default;
const deleteBlock = (await import('../../src/services/blocking/delete')).default;
alice.federateBlocks = true;
const queues = await import('../../src/queue/index');
const spy = sinon.spy(queues, 'deliver');
await createBlock(alice, carol);
await deleteBlock(alice, carol);
assert(spy.notCalled);
});
});

View file

@ -28,6 +28,10 @@
{{ i18n.ts.makeExplorable }} {{ i18n.ts.makeExplorable }}
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template> <template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
</FormSwitch> </FormSwitch>
<FormSwitch v-model="federateBlocks" @update:value="save()">
{{ $ts.federateBlocks }}
<template #caption>{{ $ts.federateBlocksDescription }}</template>
</FormSwitch>
<FormSection> <FormSection>
<FormFolder class="_formBlock"> <FormFolder class="_formBlock">
@ -69,12 +73,13 @@ let isExplorable = $ref($i.isExplorable);
let hideOnlineStatus = $ref($i.hideOnlineStatus); let hideOnlineStatus = $ref($i.hideOnlineStatus);
let publicReactions = $ref($i.publicReactions); let publicReactions = $ref($i.publicReactions);
let ffVisibility = $ref($i.ffVisibility); let ffVisibility = $ref($i.ffVisibility);
let federateBlocks = $ref($i.federateBlocks);
let defaultNoteVisibility = $computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); let defaultNoteVisibility = $computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
let defaultNoteLocalOnly = $computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); let defaultNoteLocalOnly = $computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
let keepCw = $computed(defaultStore.makeGetterSetter('keepCw')); let keepCw = $computed(defaultStore.makeGetterSetter('keepCw'));
function save() { function save(): void {
os.api('i/update', { os.api('i/update', {
isLocked: !!isLocked, isLocked: !!isLocked,
autoAcceptFollowed: !!autoAcceptFollowed, autoAcceptFollowed: !!autoAcceptFollowed,
@ -83,6 +88,7 @@ function save() {
hideOnlineStatus: !!hideOnlineStatus, hideOnlineStatus: !!hideOnlineStatus,
publicReactions: !!publicReactions, publicReactions: !!publicReactions,
ffVisibility, ffVisibility,
federateBlocks,
}); });
} }

124
yarn.lock
View file

@ -1449,7 +1449,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@sinonjs/fake-timers@npm:9.1.2": "@sinonjs/commons@npm:^2.0.0":
version: 2.0.0
resolution: "@sinonjs/commons@npm:2.0.0"
dependencies:
type-detect: 4.0.8
checksum: 5023ba17edf2b85ed58262313b8e9b59e23c6860681a9af0200f239fe939e2b79736d04a260e8270ddd57196851dde3ba754d7230be5c5234e777ae2ca8af137
languageName: node
linkType: hard
"@sinonjs/fake-timers@npm:9.1.2, @sinonjs/fake-timers@npm:^9.1.2":
version: 9.1.2 version: 9.1.2
resolution: "@sinonjs/fake-timers@npm:9.1.2" resolution: "@sinonjs/fake-timers@npm:9.1.2"
dependencies: dependencies:
@ -1458,6 +1467,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@sinonjs/fake-timers@npm:^7.0.4":
version: 7.1.2
resolution: "@sinonjs/fake-timers@npm:7.1.2"
dependencies:
"@sinonjs/commons": ^1.7.0
checksum: c84773d7973edad5511a31d2cc75023447b5cf714a84de9bb50eda45dda88a0d3bd2c30bf6e6e936da50a048d5352e2151c694e13e59b97d187ba1f329e9a00c
languageName: node
linkType: hard
"@sinonjs/fake-timers@npm:^8.0.1": "@sinonjs/fake-timers@npm:^8.0.1":
version: 8.1.0 version: 8.1.0
resolution: "@sinonjs/fake-timers@npm:8.1.0" resolution: "@sinonjs/fake-timers@npm:8.1.0"
@ -1467,6 +1485,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@sinonjs/samsam@npm:^7.0.1":
version: 7.0.1
resolution: "@sinonjs/samsam@npm:7.0.1"
dependencies:
"@sinonjs/commons": ^2.0.0
lodash.get: ^4.4.2
type-detect: ^4.0.8
checksum: 291efb158d54c67dee23ddabcb28873d22063449b692aaa3b2a4f1826d2f79d38695574063c92e9c17573cc805cd6acbf0ab0c66c9f3aed7afd0f12a2b905615
languageName: node
linkType: hard
"@sinonjs/text-encoding@npm:^0.7.1":
version: 0.7.2
resolution: "@sinonjs/text-encoding@npm:0.7.2"
checksum: fe690002a32ba06906cf87e2e8fe84d1590294586f2a7fd180a65355b53660c155c3273d8011a5f2b77209b819aa7306678ae6e4aea0df014bd7ffd4bbbcf1ab
languageName: node
linkType: hard
"@sqltools/formatter@npm:^1.2.2": "@sqltools/formatter@npm:^1.2.2":
version: 1.2.3 version: 1.2.3
resolution: "@sqltools/formatter@npm:1.2.3" resolution: "@sqltools/formatter@npm:1.2.3"
@ -2369,6 +2405,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/sinon@npm:^10.0.13":
version: 10.0.13
resolution: "@types/sinon@npm:10.0.13"
dependencies:
"@types/sinonjs__fake-timers": "*"
checksum: 46a14c888db50f0098ec53d451877e0111d878ec4a653b9e9ed7f8e54de386d6beb0e528ddc3e95cd3361a8ab9ad54e4cca33cd88d45b9227b83e9fc8fb6688a
languageName: node
linkType: hard
"@types/sinonjs__fake-timers@npm:*, @types/sinonjs__fake-timers@npm:8.1.2":
version: 8.1.2
resolution: "@types/sinonjs__fake-timers@npm:8.1.2"
checksum: bbc73a5ab6c0ec974929392f3d6e1e8db4ebad97ec506d785301e1c3d8a4f98a35b1aa95b97035daef02886fd8efd7788a2fa3ced2ec7105988bfd8dce61eedd
languageName: node
linkType: hard
"@types/sinonjs__fake-timers@npm:8.1.1": "@types/sinonjs__fake-timers@npm:8.1.1":
version: 8.1.1 version: 8.1.1
resolution: "@types/sinonjs__fake-timers@npm:8.1.1" resolution: "@types/sinonjs__fake-timers@npm:8.1.1"
@ -2376,13 +2428,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/sinonjs__fake-timers@npm:8.1.2":
version: 8.1.2
resolution: "@types/sinonjs__fake-timers@npm:8.1.2"
checksum: bbc73a5ab6c0ec974929392f3d6e1e8db4ebad97ec506d785301e1c3d8a4f98a35b1aa95b97035daef02886fd8efd7788a2fa3ced2ec7105988bfd8dce61eedd
languageName: node
linkType: hard
"@types/sizzle@npm:^2.3.2": "@types/sizzle@npm:^2.3.2":
version: 2.3.3 version: 2.3.3
resolution: "@types/sizzle@npm:2.3.3" resolution: "@types/sizzle@npm:2.3.3"
@ -3657,6 +3702,7 @@ __metadata:
"@types/sanitize-html": 2.6.2 "@types/sanitize-html": 2.6.2
"@types/semver": 7.3.12 "@types/semver": 7.3.12
"@types/sharp": 0.30.5 "@types/sharp": 0.30.5
"@types/sinon": ^10.0.13
"@types/sinonjs__fake-timers": 8.1.2 "@types/sinonjs__fake-timers": 8.1.2
"@types/speakeasy": 2.0.7 "@types/speakeasy": 2.0.7
"@types/tinycolor2": 1.4.3 "@types/tinycolor2": 1.4.3
@ -3744,6 +3790,7 @@ __metadata:
sanitize-html: 2.7.0 sanitize-html: 2.7.0
semver: 7.3.7 semver: 7.3.7
sharp: 0.31.2 sharp: 0.31.2
sinon: ^14.0.2
speakeasy: 2.0.0 speakeasy: 2.0.0
strict-event-emitter-types: 2.0.0 strict-event-emitter-types: 2.0.0
stringz: 2.1.0 stringz: 2.1.0
@ -5917,6 +5964,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"diff@npm:^5.0.0":
version: 5.1.0
resolution: "diff@npm:5.1.0"
checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90
languageName: node
linkType: hard
"dijkstrajs@npm:^1.0.1": "dijkstrajs@npm:^1.0.1":
version: 1.0.2 version: 1.0.2
resolution: "dijkstrajs@npm:1.0.2" resolution: "dijkstrajs@npm:1.0.2"
@ -9764,6 +9818,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"isarray@npm:0.0.1":
version: 0.0.1
resolution: "isarray@npm:0.0.1"
checksum: 49191f1425681df4a18c2f0f93db3adb85573bcdd6a4482539d98eac9e705d8961317b01175627e860516a2fc45f8f9302db26e5a380a97a520e272e2a40a8d4
languageName: node
linkType: hard
"isarray@npm:1.0.0, isarray@npm:^1.0.0, isarray@npm:~1.0.0": "isarray@npm:1.0.0, isarray@npm:^1.0.0, isarray@npm:~1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "isarray@npm:1.0.0" resolution: "isarray@npm:1.0.0"
@ -10808,6 +10869,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"just-extend@npm:^4.0.2":
version: 4.2.1
resolution: "just-extend@npm:4.2.1"
checksum: ff9fdede240fad313efeeeb68a660b942e5586d99c0058064c78884894a2690dc09bba44c994ad4e077e45d913fef01a9240c14a72c657b53687ac58de53b39c
languageName: node
linkType: hard
"jwa@npm:^2.0.0": "jwa@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "jwa@npm:2.0.0" resolution: "jwa@npm:2.0.0"
@ -12262,6 +12330,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"nise@npm:^5.1.2":
version: 5.1.2
resolution: "nise@npm:5.1.2"
dependencies:
"@sinonjs/commons": ^2.0.0
"@sinonjs/fake-timers": ^7.0.4
"@sinonjs/text-encoding": ^0.7.1
just-extend: ^4.0.2
path-to-regexp: ^1.7.0
checksum: 688c557333dcbc5b41f4f1f1b0ea32fb0f8b424541a8958140bc61074980362c954b2aeb027c282de26b9ddcb4b230656f68ac4206777499e405dd7e716ec1f8
languageName: node
linkType: hard
"node-abi@npm:^3.3.0": "node-abi@npm:^3.3.0":
version: 3.24.0 version: 3.24.0
resolution: "node-abi@npm:3.24.0" resolution: "node-abi@npm:3.24.0"
@ -13133,6 +13214,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"path-to-regexp@npm:^1.7.0":
version: 1.8.0
resolution: "path-to-regexp@npm:1.8.0"
dependencies:
isarray: 0.0.1
checksum: 709f6f083c0552514ef4780cb2e7e4cf49b0cc89a97439f2b7cc69a608982b7690fb5d1720a7473a59806508fc2dae0be751ba49f495ecf89fd8fbc62abccbcd
languageName: node
linkType: hard
"path-to-regexp@npm:^6.1.0": "path-to-regexp@npm:^6.1.0":
version: 6.2.1 version: 6.2.1
resolution: "path-to-regexp@npm:6.2.1" resolution: "path-to-regexp@npm:6.2.1"
@ -15353,6 +15443,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sinon@npm:^14.0.2":
version: 14.0.2
resolution: "sinon@npm:14.0.2"
dependencies:
"@sinonjs/commons": ^2.0.0
"@sinonjs/fake-timers": ^9.1.2
"@sinonjs/samsam": ^7.0.1
diff: ^5.0.0
nise: ^5.1.2
supports-color: ^7.2.0
checksum: de7730cd7785a457e42f9a93e955780c870296036a13816e3c0c5648360afae82fdc748e36c854cf26fb8abd117855a7211aee49265c334fa61439aae17a1b72
languageName: node
linkType: hard
"sisteransi@npm:^1.0.5": "sisteransi@npm:^1.0.5":
version: 1.0.5 version: 1.0.5
resolution: "sisteransi@npm:1.0.5" resolution: "sisteransi@npm:1.0.5"
@ -16054,7 +16158,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": "supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0":
version: 7.2.0 version: 7.2.0
resolution: "supports-color@npm:7.2.0" resolution: "supports-color@npm:7.2.0"
dependencies: dependencies:
@ -16764,7 +16868,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"type-detect@npm:4.0.8": "type-detect@npm:4.0.8, type-detect@npm:^4.0.8":
version: 4.0.8 version: 4.0.8
resolution: "type-detect@npm:4.0.8" resolution: "type-detect@npm:4.0.8"
checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15 checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15