feat: 特定ロールポリシーによる運営アクティビティの検知範囲拡大に対応

This commit is contained in:
おさむのひと 2024-11-04 21:09:33 +09:00
parent eef0c895bc
commit 0d0379def0
14 changed files with 444 additions and 27 deletions

4
locales/index.d.ts vendored
View file

@ -6989,6 +6989,10 @@ export interface Locale extends ILocale {
*
*/
"canImportUserLists": string;
/**
*
*/
"isModeratorInactivityCheckTarget": string;
};
"_condition": {
/**

View file

@ -1806,6 +1806,7 @@ _role:
canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可"
isModeratorInactivityCheckTarget: "モデレーターの活動状況チェックの対象に含める"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"

View file

@ -63,6 +63,7 @@ export type RolePolicies = {
canImportFollowing: boolean;
canImportMuting: boolean;
canImportUserLists: boolean;
isModeratorInactivityCheckTarget: boolean;
};
export const DEFAULT_POLICIES: RolePolicies = {
@ -97,6 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canImportFollowing: true,
canImportMuting: true,
canImportUserLists: true,
isModeratorInactivityCheckTarget: false,
};
@Injectable()
@ -402,9 +404,23 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
isModeratorInactivityCheckTarget: calc('isModeratorInactivityCheckTarget', vs => vs.some(v => v === true)),
};
}
@bindThis
public async getUsersByRoleIds(roleIds: MiRole['id'][]): Promise<MiUser[]> {
// 今のところこの関数の使用頻度は低めなのでキャッシュは作らない.
// 使用頻度が増えた場合はroleAssignmentByUserIdCacheのようなキャッシュを作るべきか否かを検討する必要がある.
const users = await this.roleAssignmentsRepository.createQueryBuilder('roleAssignment')
.innerJoinAndSelect('roleAssignment.user', 'user')
.where('roleAssignment.roleId IN (:...roleIds)', { roleIds })
.getMany()
.then(it => it.map(it => it.user).filter(it => it != null));
return [...new Map(users.map(it => [it.id, it])).values()];
}
@bindThis
public async isModerator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
if (user == null) return false;
@ -465,6 +481,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
if (includeRoot) {
const rootUserId = await this.rootUserIdCache.fetch(async () => {
// rootは必ず1人存在するという前提のもと
const it = await this.usersRepository.createQueryBuilder('users')
.select('id')
.where({ isRoot: true })
@ -687,6 +704,17 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
}
/**
* Service内部で保持しているキャッシュをすべて削除する.
* .
*/
@bindThis
public flushCaches(): void {
this.rootUserIdCache.delete();
this.rolesCache.delete();
this.roleAssignmentByUserIdCache.deleteAll();
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);

View file

@ -242,6 +242,11 @@ export class MemoryKVCache<T> {
this.cache.delete(key);
}
@bindThis
public deleteAll() {
this.cache.clear();
}
/**
* fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします

View file

@ -292,6 +292,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
isModeratorInactivityCheckTarget: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View file

@ -8,7 +8,7 @@ import { In } from 'typeorm';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { RoleService } from '@/core/RoleService.js';
import { RolePolicies, RoleService } from '@/core/RoleService.js';
import { EmailService } from '@/core/EmailService.js';
import { MiUser, type UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
@ -281,12 +281,47 @@ export class CheckModeratorsActivityProcessorService {
}
@bindThis
private async fetchModerators() {
// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
return this.roleService.getModerators({
includeAdmins: true,
includeRoot: true,
excludeExpire: true,
});
public async fetchModerators() {
const resultMap = await this.roleService
.getModerators({ includeAdmins: true, includeRoot: true, excludeExpire: true })
.then(it => new Map(it.map(it => [it.id, it])));
const additionalUsers = await this.fetchAdditionalTargetUsers();
for (const user of additionalUsers) {
resultMap.set(user.id, user);
}
return [...resultMap.values()];
}
@bindThis
private async fetchAdditionalTargetUsers() {
const roles = await this.roleService.getRoles();
const targetRoleIds = roles
.filter(it => (it.policies as unknown as Partial<RolePolicies>).isModeratorInactivityCheckTarget ?? false)
.map(it => it.id);
if (targetRoleIds.length === 0) {
// 該当ポリシーが有効なロールが存在しない
return [];
}
const tmpTargetUsers = await this.roleService.getUsersByRoleIds(targetRoleIds)
.then(it => [...new Map(it.map(it => [it.id, it])).values()]);
if (tmpTargetUsers.length === 0) {
// 該当ポリシーが有効なロールにアサインされたユーザが存在しない
return [];
}
const tmpTargetUsersWithPolicies = await Promise.all(
tmpTargetUsers.map(async user => {
// 複数ロールを組み合わせた最終的なポリシーを計算する必要がある
const policies = await this.roleService.getUserPolicies(user.id);
return { user, policies };
}),
);
return tmpTargetUsersWithPolicies
.filter(it => it.policies.isModeratorInactivityCheckTarget)
.map(it => it.user);
}
}

View file

@ -480,6 +480,27 @@ describe('RoleService', () => {
});
});
describe('getUsersByRoleIds', () => {
test('get users by role ids', async () => {
const [user1, user2, user3, role1, role2, role3] = await Promise.all([
createUser(),
createUser(),
createUser(),
createRole(),
createRole(),
createRole(),
]);
await Promise.all([
roleService.assign(user1.id, role1.id),
roleService.assign(user2.id, role1.id),
roleService.assign(user3.id, role2.id),
]);
const result = await roleService.getUsersByRoleIds([role1.id]);
expect(result.map(u => u.id)).toEqual([user1.id, user2.id]);
});
});
describe('conditional role', () => {
test('~かつ~', async () => {
const [user1, user2, user3, user4] = await Promise.all([

View file

@ -8,7 +8,17 @@ import { Test, TestingModule } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { MiSystemWebhook, MiUser, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import {
MiRole,
MiRoleAssignment,
MiSystemWebhook,
MiUser,
MiUserProfile,
RoleAssignmentsRepository,
RolesRepository,
UserProfilesRepository,
UsersRepository,
} from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { RoleService } from '@/core/RoleService.js';
import { GlobalModule } from '@/GlobalModule.js';
@ -18,6 +28,12 @@ import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
import { EmailService } from '@/core/EmailService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { genAidx } from '@/misc/id/aidx.js';
import { CacheService } from '@/core/CacheService.js';
const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
@ -93,7 +109,10 @@ describe('CheckModeratorsActivityProcessorService', () => {
CheckModeratorsActivityProcessorService,
IdService,
{
provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }),
provide: RoleService, useFactory: () => ({
getModerators: jest.fn(),
getRoles: () => Promise.resolve([]),
}),
},
{
provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
@ -377,3 +396,269 @@ describe('CheckModeratorsActivityProcessorService', () => {
});
});
});
// 本物のRoleServiceと結合しないと出来ないテスト
describe('CheckModeratorsActivityProcessorService with RoleService', () => {
let app: TestingModule;
let clock: lolex.InstalledClock;
let service: CheckModeratorsActivityProcessorService;
// --------------------------------------------------------------------------------------
let usersRepository: UsersRepository;
let userProfilesRepository: UserProfilesRepository;
let rolesRepository: RolesRepository;
let roleAssignmentsRepository: RoleAssignmentsRepository;
let idService: IdService;
let roleService: RoleService;
let root: MiUser;
// --------------------------------------------------------------------------------------
async function createUser(data: Partial<MiUser> = {}, profile: Partial<MiUserProfile> = {}): Promise<MiUser> {
const id = idService.gen();
const user = await usersRepository
.insert({
id: id,
username: `user_${id}`,
usernameLower: `user_${id}`.toLowerCase(),
...data,
})
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
await userProfilesRepository.insert({
userId: user.id,
...profile,
});
return user;
}
async function createRole(data: Partial<MiRole> = {}) {
const x = await rolesRepository.insert({
id: genAidx(Date.now()),
updatedAt: new Date(),
lastUsedAt: new Date(),
name: '',
description: '',
...data,
});
return await rolesRepository.findOneByOrFail(x.identifiers[0]);
}
async function assignRole(args: Partial<MiRoleAssignment>) {
const id = genAidx(Date.now());
await roleAssignmentsRepository.insert({
id,
...args,
});
return await roleAssignmentsRepository.findOneByOrFail({ id });
}
// --------------------------------------------------------------------------------------
beforeAll(async () => {
app = await Test
.createTestingModule({
imports: [
GlobalModule,
],
providers: [
CheckModeratorsActivityProcessorService,
IdService,
RoleService,
GlobalEventService,
CacheService,
{
provide: ModerationLogService, useFactory: () => ({ log: jest.fn() }),
},
{
provide: FanoutTimelineService, useFactory: () => ({ push: jest.fn() }),
},
{
provide: UserEntityService, useFactory: () => ({ pack: jest.fn() }),
},
{
provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
},
{
provide: AnnouncementService, useFactory: () => ({ create: jest.fn() }),
},
{
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
},
{
provide: SystemWebhookService, useFactory: () => ({
fetchActiveSystemWebhooks: jest.fn(),
enqueueSystemWebhook: jest.fn(),
}),
},
{
provide: QueueLoggerService, useFactory: () => ({
logger: ({
createSubLogger: () => ({
info: jest.fn(),
warn: jest.fn(),
succ: jest.fn(),
}),
}),
}),
},
],
})
.compile();
usersRepository = app.get(DI.usersRepository);
userProfilesRepository = app.get(DI.userProfilesRepository);
rolesRepository = app.get<RolesRepository>(DI.rolesRepository);
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
service = app.get(CheckModeratorsActivityProcessorService);
idService = app.get(IdService);
roleService = app.get(RoleService);
app.enableShutdownHooks();
});
beforeEach(async () => {
clock = lolex.install({
now: new Date(baseDate),
shouldClearNativeTimers: true,
});
root = await createUser({ isRoot: true, lastActiveDate: new Date() });
});
afterEach(async () => {
clock.uninstall();
await usersRepository.delete({});
await userProfilesRepository.delete({});
await roleAssignmentsRepository.delete({});
await rolesRepository.delete({});
roleService.flushCaches();
});
afterAll(async () => {
await app.close();
});
// --------------------------------------------------------------------------------------
describe('fetchModerators', () => {
function expectUsers(users: MiUser[], expected: MiUser[]) {
expect(users.sort((x, y) => x.id.localeCompare(y.id)))
.toEqual(expected.sort((x, y) => x.id.localeCompare(y.id)));
}
test('モデレーターロール無し -> root', async () => {
const [user1, user2, user3] = await Promise.all([
createUser(),
createUser(),
createUser(),
]);
const [role1, role2] = await Promise.all([
createRole({ isModerator: false }),
createRole({ isModerator: false }),
]);
await Promise.all([
assignRole({ userId: user1.id, roleId: role1.id }),
assignRole({ userId: user2.id, roleId: role2.id }),
assignRole({ userId: user3.id, roleId: role1.id }),
assignRole({ userId: user3.id, roleId: role2.id }),
]);
const result = await service.fetchModerators();
expectUsers(result, [root]);
});
test('モデレーターロール有り -> root, user2, user3', async () => {
const [user1, user2, user3] = await Promise.all([
createUser(),
createUser(),
createUser(),
]);
const [role1, role2] = await Promise.all([
createRole({ isModerator: false }),
createRole({ isModerator: true }),
]);
await Promise.all([
assignRole({ userId: user1.id, roleId: role1.id }),
assignRole({ userId: user2.id, roleId: role2.id }),
assignRole({ userId: user3.id, roleId: role1.id }),
assignRole({ userId: user3.id, roleId: role2.id }),
]);
const result = await service.fetchModerators();
expectUsers(result, [root, user2, user3]);
});
test('モデレーターロール無し + 特殊ポリシーロール -> root, user1, user3', async () => {
const [user1, user2, user3] = await Promise.all([
createUser(),
createUser(),
createUser(),
]);
const [role1, role2] = await Promise.all([
createRole({
isModerator: false, policies: {
isModeratorInactivityCheckTarget: {
useDefault: false,
value: true,
priority: 0,
},
},
}),
createRole({ isModerator: false }),
]);
await Promise.all([
assignRole({ userId: user1.id, roleId: role1.id }),
assignRole({ userId: user2.id, roleId: role2.id }),
assignRole({ userId: user3.id, roleId: role1.id }),
assignRole({ userId: user3.id, roleId: role2.id }),
]);
const result = await service.fetchModerators();
expectUsers(result, [root, user1, user3]);
});
test('モデレーターロールあり + 特殊ポリシーロール -> root, user1, user2, user3', async () => {
const [user1, user2, user3] = await Promise.all([
createUser(),
createUser(),
createUser(),
]);
const [role1, role2] = await Promise.all([
createRole({
isModerator: false, policies: {
isModeratorInactivityCheckTarget: {
useDefault: false,
value: true,
priority: 0,
},
},
}),
createRole({ isModerator: true }),
]);
await Promise.all([
assignRole({ userId: user1.id, roleId: role1.id }),
assignRole({ userId: user2.id, roleId: role2.id }),
assignRole({ userId: user3.id, roleId: role1.id }),
assignRole({ userId: user3.id, roleId: role2.id }),
]);
const result = await service.fetchModerators();
expectUsers(result, [root, user1, user2, user3]);
});
});
});

View file

@ -106,6 +106,7 @@ export const ROLE_POLICIES = [
'canImportFollowing',
'canImportMuting',
'canImportUserLists',
'isModeratorInactivityCheckTarget',
] as const;
// なんか動かない

View file

@ -690,6 +690,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.isModeratorInactivityCheckTarget, 'isModeratorInactivityCheckTarget'])">
<template #label>{{ i18n.ts._role._options.isModeratorInactivityCheckTarget }}</template>
<template #suffix>
<span v-if="role.policies.isModeratorInactivityCheckTarget.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.isModeratorInactivityCheckTarget.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.isModeratorInactivityCheckTarget)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.isModeratorInactivityCheckTarget.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.isModeratorInactivityCheckTarget.value" :disabled="role.policies.isModeratorInactivityCheckTarget.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.isModeratorInactivityCheckTarget.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
</div>
</FormSlot>
</div>
@ -698,6 +718,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
import { throttle } from 'throttle-debounce';
import { ROLE_POLICIES } from '@@/js/const.js';
import RolesEditorFormula from './RolesEditorFormula.vue';
import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
@ -708,7 +729,6 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue';
import { i18n } from '@/i18n.js';
import { ROLE_POLICIES } from '@@/js/const.js';
import { instance } from '@/instance.js';
import { deepClone } from '@/scripts/clone.js';

View file

@ -256,6 +256,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.isModeratorInactivityCheckTarget, 'isModeratorInactivityCheckTarget'])">
<template #label>{{ i18n.ts._role._options.isModeratorInactivityCheckTarget }}</template>
<template #suffix>{{ policies.isModeratorInactivityCheckTarget ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.isModeratorInactivityCheckTarget">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
</div>
</MkFolder>
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>

View file

@ -11,6 +11,7 @@
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"eslint": "9.14.0",
"openapi-types": "12.1.3",
"openapi-typescript": "6.7.3",
"ts-case-convert": "2.1.0",

View file

@ -4881,6 +4881,7 @@ export type components = {
canImportFollowing: boolean;
canImportMuting: boolean;
canImportUserLists: boolean;
isModeratorInactivityCheckTarget: boolean;
};
ReversiGameLite: {
/** Format: id */

View file

@ -1368,6 +1368,9 @@ importers:
'@typescript-eslint/parser':
specifier: 7.17.0
version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
eslint:
specifier: 9.14.0
version: 9.14.0
openapi-types:
specifier: 12.1.3
version: 12.1.3
@ -11780,7 +11783,7 @@ snapshots:
'@babel/traverse': 7.23.5
'@babel/types': 7.24.7
convert-source-map: 2.0.0
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -11800,7 +11803,7 @@ snapshots:
'@babel/traverse': 7.24.7
'@babel/types': 7.24.7
convert-source-map: 2.0.0
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -12059,7 +12062,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.25.6
'@babel/types': 7.24.7
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -12074,7 +12077,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.24.7
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -12465,7 +12468,7 @@ snapshots:
'@eslint/config-array@0.18.0':
dependencies:
'@eslint/object-schema': 2.1.4
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@ -12475,7 +12478,7 @@ snapshots:
'@eslint/eslintrc@3.1.0':
dependencies:
ajv: 6.12.6
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.1
@ -15637,7 +15640,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
optional: true
@ -17248,7 +17251,7 @@ snapshots:
esbuild-register@3.5.0(esbuild@0.24.0):
dependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
esbuild: 0.24.0
transitivePeerDependencies:
- supports-color
@ -17490,7 +17493,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
escape-string-regexp: 4.0.0
eslint-scope: 8.2.0
eslint-visitor-keys: 4.2.0
@ -17935,7 +17938,7 @@ snapshots:
follow-redirects@1.15.9(debug@4.3.7):
optionalDependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
for-each@0.3.3:
dependencies:
@ -18805,7 +18808,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@ -19236,7 +19239,7 @@ snapshots:
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.0.0
ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
@ -19936,7 +19939,7 @@ snapshots:
micromark@4.0.0:
dependencies:
'@types/debug': 4.1.12
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.0
@ -21396,7 +21399,7 @@ snapshots:
require-in-the-middle@7.3.0:
dependencies:
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
module-details-from-path: 1.0.3
resolve: 1.22.8
transitivePeerDependencies:
@ -21821,7 +21824,7 @@ snapshots:
socks-proxy-agent@8.0.2:
dependencies:
agent-base: 7.1.0
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
socks: 2.7.1
transitivePeerDependencies:
- supports-color
@ -21930,7 +21933,7 @@ snapshots:
arg: 5.0.2
bluebird: 3.7.2
check-more-types: 2.24.0
debug: 4.3.7(supports-color@5.5.0)
debug: 4.3.7(supports-color@8.1.1)
execa: 5.1.1
lazy-ass: 1.6.0
ps-tree: 1.2.0