From 85ca2269e448b977b35e30517f9f1f916ef44489 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 12 Aug 2025 23:40:49 -0400 Subject: [PATCH] completely re-implement note visibility as NoteVisibilityService --- packages/backend/src/core/CoreModule.ts | 6 + .../src/core/FanoutTimelineEndpointService.ts | 82 +--- .../backend/src/core/NoteVisibilityService.ts | 420 ++++++++++++++++++ packages/backend/src/core/QueryService.ts | 63 ++- packages/backend/src/core/ReactionService.ts | 5 +- .../backend/src/core/WebhookTestService.ts | 1 + .../src/core/activitypub/ApInboxService.ts | 5 +- .../src/core/entities/NoteEntityService.ts | 83 ++-- .../backend/src/models/json-schema/note.ts | 4 + .../api/endpoints/notes/bubble-timeline.ts | 2 +- .../src/server/api/endpoints/notes/create.ts | 6 +- .../src/server/api/endpoints/notes/edit.ts | 4 +- .../server/api/endpoints/notes/translate.ts | 5 +- .../src/server/api/endpoints/users/notes.ts | 2 +- .../backend/src/server/api/stream/channel.ts | 41 +- .../src/server/api/stream/channels/antenna.ts | 3 +- .../api/stream/channels/bubble-timeline.ts | 28 +- .../src/server/api/stream/channels/channel.ts | 7 +- .../api/stream/channels/global-timeline.ts | 29 +- .../src/server/api/stream/channels/hashtag.ts | 3 +- .../api/stream/channels/home-timeline.ts | 27 +- .../api/stream/channels/hybrid-timeline.ts | 28 +- .../api/stream/channels/local-timeline.ts | 30 +- .../src/server/api/stream/channels/main.ts | 23 +- .../api/stream/channels/role-timeline.ts | 24 +- .../server/api/stream/channels/user-list.ts | 29 +- pnpm-lock.yaml | 385 ++++++++++++---- 27 files changed, 925 insertions(+), 420 deletions(-) create mode 100644 packages/backend/src/core/NoteVisibilityService.ts diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 6839ba0159..f818a65ff8 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -20,6 +20,7 @@ import { EnvService } from '@/core/EnvService.js'; import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { ApLogService } from '@/core/ApLogService.js'; import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js'; +import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AnnouncementService } from './AnnouncementService.js'; @@ -240,6 +241,7 @@ const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisti const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService }; const $TimeService: Provider = { provide: 'TimeService', useExisting: TimeService }; const $EnvService: Provider = { provide: 'EnvService', useExisting: EnvService }; +const $NoteVisibilityService: Provider = { provide: 'NoteVisibilityService', useExisting: NoteVisibilityService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -400,6 +402,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp ReversiService, TimeService, EnvService, + NoteVisibilityService, ChartLoggerService, FederationChart, @@ -556,6 +559,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp $ReversiService, $TimeService, $EnvService, + $NoteVisibilityService, $ChartLoggerService, $FederationChart, @@ -713,6 +717,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp ReversiService, TimeService, EnvService, + NoteVisibilityService, FederationChart, NotesChart, @@ -867,6 +872,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp $ReversiService, $TimeService, $EnvService, + $NoteVisibilityService, $FederationChart, $NotesChart, diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 7699df4ad6..1a4af8cf1e 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -15,10 +15,11 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; import { CacheService } from '@/core/CacheService.js'; import { isReply } from '@/misc/is-reply.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; +import { NoteVisibilityService, PopulatedNote } from '@/core/NoteVisibilityService.js'; type TimelineOptions = { untilId: string | null, @@ -56,6 +57,7 @@ export class FanoutTimelineEndpointService { private cacheService: CacheService, private fanoutTimelineService: FanoutTimelineService, private utilityService: UtilityService, + private readonly noteVisibilityService: NoteVisibilityService, ) { } @@ -110,52 +112,14 @@ export class FanoutTimelineEndpointService { filter = (note) => (!isRenote(note) || isQuote(note)) && parentFilter(note); } - if (ps.me) { - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - userMutedInstances, - myFollowings, - myThreadMutings, - myNoteMutings, - me, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(ps.me.id), - this.cacheService.renoteMutingsCache.fetch(ps.me.id), - this.cacheService.userBlockedCache.fetch(ps.me.id), - this.cacheService.userProfileCache.fetch(ps.me.id).then(p => new Set(p.mutedInstances)), - this.cacheService.userFollowingsCache.fetch(ps.me.id).then(fs => new Set(fs.keys())), - this.cacheService.threadMutingsCache.fetch(ps.me.id), - this.cacheService.noteMutingsCache.fetch(ps.me.id), - this.cacheService.findUserById(ps.me.id), - ]); + { + const me = ps.me ? await this.cacheService.findUserById(ps.me.id) : null; + const data = await this.noteVisibilityService.populateData(me); const parentFilter = filter; filter = (note) => { - if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false; - if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; - if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false; - if (isInstanceMuted(note, userMutedInstances)) return false; - - // Silenced users (when logged in) - if (!ps.ignoreAuthorFromUserSilence && !myFollowings.has(note.userId)) { - if (note.user?.isSilenced || note.user?.instance?.isSilenced) return false; - if (note.reply?.user?.isSilenced || note.reply?.user?.instance?.isSilenced) return false; - if (note.renote?.user?.isSilenced || note.renote?.user?.instance?.isSilenced) return false; - } - - // Muted threads / posts - if (!ps.includeMutedNotes) { - if (myThreadMutings.has(note.threadId ?? note.id) || myNoteMutings.has(note.id)) return false; - if (note.replyId && myNoteMutings.has(note.replyId)) return false; - if (note.renote && (myThreadMutings.has(note.renote.threadId ?? note.renote.id) || myNoteMutings.has(note.renote.id))) return false; - } - - // Invisible notes - if (!this.noteEntityService.isVisibleForMeSync(note, me, myFollowings, userIdsWhoBlockingMe)) { - return false; - } + const { accessible, silence } = this.noteVisibilityService.checkNoteVisibility(note as PopulatedNote, me, { data, filters: { includeSilencedAuthor: ps.ignoreAuthorFromUserSilence } }); + if (!accessible || silence) return false; return parentFilter(note); }; @@ -191,36 +155,6 @@ export class FanoutTimelineEndpointService { }; } - { - const parentFilter = filter; - filter = (note) => { - // Silenced users (when logged out) - if (!ps.ignoreAuthorFromUserSilence && !ps.me) { - if (note.user?.isSilenced || note.user?.instance?.isSilenced) return false; - if (note.reply?.user?.isSilenced || note.reply?.user?.instance?.isSilenced) return false; - if (note.renote?.user?.isSilenced || note.renote?.user?.instance?.isSilenced) return false; - } - - return parentFilter(note); - }; - } - - // This one MUST be last! - { - const parentFilter = filter; - filter = (note) => { - // If this is a boost, then first run all checks for the boost target. - if (isPureRenote(note) && note.renote) { - if (!parentFilter(note.renote)) { - return false; - } - } - - // Either way, make sure to run the checks for the actual note too! - return parentFilter(note); - }; - } - const redisTimeline: MiNote[] = []; let readFromRedis = 0; let lastSuccessfulRate = 1; // rateをキャッシュする? diff --git a/packages/backend/src/core/NoteVisibilityService.ts b/packages/backend/src/core/NoteVisibilityService.ts new file mode 100644 index 0000000000..8df83b4c5e --- /dev/null +++ b/packages/backend/src/core/NoteVisibilityService.ts @@ -0,0 +1,420 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { CacheService } from '@/core/CacheService.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiUser } from '@/models/User.js'; +import { MiInstance } from '@/models/Instance.js'; +import { bindThis } from '@/decorators.js'; +import { Packed } from '@/misc/json-schema.js'; +import { IdService } from '@/core/IdService.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import type { MiFollowing, NotesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; + +/** + * Visibility level for a given user towards a given post. + */ +export interface NoteVisibilityResult { + /** + * Whether the user has access to view this post. + */ + accessible: boolean; + + /** + * If the user should be shown only a redacted version of the post. + * (see NoteEntityService.hideNote() for details.) + */ + redact: boolean; + + /** + * If false, the note should be visible by default. (normal case) + * If true, the note should be hidden by default. (Silences, mutes, etc.) + * If "timeline", the note should be hidden in timelines only. (following w/o replies) + */ + silence: boolean; +} + +export interface NoteVisibilityFilters { + /** + * If false, exclude replies to other users unless the "include replies to others in timeline" has been enabled for the note's author. + * If true (default), then replies are treated like any other post. + */ + includeReplies?: boolean; + + /** + * If true, treat the note's author as never being silenced. Does not apply to reply or renote targets, unless they're by the same author. + * If false (default), then silence is enforced for all notes. + */ + includeSilencedAuthor?: boolean; +} + +@Injectable() +export class NoteVisibilityService { + constructor( + @Inject(DI.notesRepository) + private readonly notesRepository: NotesRepository, + + private readonly cacheService: CacheService, + private readonly idService: IdService, + private readonly federatedInstanceService: FederatedInstanceService, + ) {} + + @bindThis + public async checkNoteVisibilityAsync(note: MiNote | Packed<'Note'>, user: string | PopulatedUser, opts?: { filters?: NoteVisibilityFilters, hint?: Partial }): Promise { + if (typeof(user) === 'string') { + user = await this.cacheService.findUserById(user); + } + + const populatedNote = await this.populateNote(note); + const populatedData = await this.populateData(user, opts?.hint ?? {}); + + return this.checkNoteVisibility(populatedNote, user, { filters: opts?.filters, data: populatedData }); + } + + private async populateNote(note: Packed<'Note'>, dive?: boolean): Promise>; + private async populateNote(note: MiNote, dive?: boolean): Promise; + private async populateNote(note: MiNote | Packed<'Note'>, dive?: boolean): Promise; + private async populateNote(note: MiNote | Packed<'Note'>, dive = true): Promise { + // Packed<'Note'> is already fully loaded + if (isPackedNote(note)) return note; + + // noinspection ES6MissingAwait + return await awaitAll({ + ...note, + user: this.getNoteUser(note), + renote: dive ? this.getNoteRenote(note) : null, + reply: dive ? this.getNoteReply(note) : null, + }); + } + + private async getNoteUser(note: MiNote): Promise { + const user = note.user ?? await this.cacheService.findUserById(note.userId); + return { + ...user, + instance: user.instance ?? (user.host ? await this.federatedInstanceService.fetchOrRegister(user.host) : null), + }; + } + + private async getNoteRenote(note: MiNote): Promise { + if (!note.renoteId) return null; + + const renote = note.renote ?? await this.notesRepository.findOneOrFail({ + where: { id: note.renoteId }, + relations: { + user: { + instance: true, + }, + }, + }); + + return await this.populateNote(renote, false); + } + + private async getNoteReply(note: MiNote): Promise { + if (!note.replyId) return null; + + const reply = note.reply ?? await this.notesRepository.findOneOrFail({ + where: { id: note.replyId }, + relations: { + user: { + instance: true, + }, + }, + }); + + return await this.populateNote(reply, false); + } + + @bindThis + public async populateData(user: PopulatedUser, hint?: Partial): Promise { + // noinspection ES6MissingAwait + const [ + userBlockers, + userFollowings, + userMutedThreads, + userMutedNotes, + userMutedUsers, + userMutedUserRenotes, + userMutedInstances, + ] = await Promise.all([ + user ? (hint?.userBlockers ?? this.cacheService.userBlockedCache.fetch(user.id)) : null, + user ? (hint?.userFollowings ?? this.cacheService.userFollowingsCache.fetch(user.id)) : null, + user ? (hint?.userMutedThreads ?? this.cacheService.threadMutingsCache.fetch(user.id)) : null, + user ? (hint?.userMutedNotes ?? this.cacheService.noteMutingsCache.fetch(user.id)) : null, + user ? (hint?.userMutedUsers ?? this.cacheService.userMutingsCache.fetch(user.id)) : null, + user ? (hint?.userMutedUserRenotes ?? this.cacheService.renoteMutingsCache.fetch(user.id)) : null, + user ? (hint?.userMutedInstances ?? this.cacheService.userProfileCache.fetch(user.id).then(p => new Set(p.mutedInstances))) : null, + ]); + + return { + userBlockers, + userFollowings, + userMutedThreads, + userMutedNotes, + userMutedUsers, + userMutedUserRenotes, + userMutedInstances, + }; + } + + @bindThis + public checkNoteVisibility(note: PopulatedNote, user: PopulatedUser, opts: { filters?: NoteVisibilityFilters, data: NoteVisibilityData }): NoteVisibilityResult { + // Copy note since we mutate it below + note = { + ...note, + renote: note.renote ? { ...note.renote } : null, + reply: note.reply ? { ...note.reply } : null, + } as PopulatedNote; + + this.syncVisibility(note); + + const accessible = this.isAccessible(note, user, opts.data); + const redact = this.shouldRedact(note, user, opts.data); + const silence = this.shouldSilence(note, user, opts.data, opts.filters); + + const baseVisibility = { accessible, redact, silence }; + + // For boosts (pure renotes), we must recurse and pick the lowest common access level. + if (isPopulatedBoost(note)) { + const boostVisibility = this.checkNoteVisibility(note.renote, user, opts); + return { + accessible: baseVisibility.accessible && boostVisibility.accessible, + redact: baseVisibility.redact || boostVisibility.redact, + silence: baseVisibility.silence || boostVisibility.silence, + }; + } + + return baseVisibility; + } + + // Based on NoteEntityService.isVisibleForMe + private isAccessible(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData): boolean { + // We can always view our own notes + if (user?.id === note.userId) return true; + + // We can *never* view blocked notes + if (data.userBlockers?.has(note.userId)) return false; + + if (note.visibility === 'specified') { + return this.isAccessibleDM(note, user); + } else if (note.visibility === 'followers') { + return this.isAccessibleFO(note, user, data); + } else { + return true; + } + } + + private isAccessibleDM(note: PopulatedNote, user: PopulatedUser): boolean { + // Must be logged in to view DM + if (user == null) return false; + + // Can be visible to me + if (note.visibleUserIds?.includes(user.id)) return true; + + // Otherwise invisible + return false; + } + + private isAccessibleFO(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData): boolean { + // Must be logged in to view FO + if (user == null) return false; + + // Can be a reply to me + if (note.reply?.userId === user.id) return true; + + // Can mention me + if (note.mentions?.includes(user.id)) return true; + + // Can be visible to me + if (note.visibleUserIds?.includes(user.id)) return true; + + // Can be followed by me + if (data.userFollowings?.has(note.userId)) return true; + + // Can be two remote users, since we can't verify remote->remote following. + if (note.userHost != null && user.host != null) return true; + + // Otherwise invisible + return false; + } + + // Based on NoteEntityService.treatVisibility + @bindThis + public syncVisibility(note: PopulatedNote): void { + // Make followers-only + if (note.user.makeNotesFollowersOnlyBefore && note.visibility !== 'specified' && note.visibility !== 'followers') { + const followersOnlyBefore = note.user.makeNotesFollowersOnlyBefore * 1000; + const createdAt = 'createdAt' in note + ? new Date(note.createdAt).getTime() + : this.idService.parse(note.id).date.getTime(); + + // I don't understand this logic, but I tried to break it out for readability + const followersOnlyOpt1 = followersOnlyBefore <= 0 && (Date.now() - createdAt > 0 - followersOnlyBefore); + const followersOnlyOpt2 = followersOnlyBefore > 0 && (createdAt < followersOnlyBefore); + if (followersOnlyOpt1 || followersOnlyOpt2) { + note.visibility = 'followers'; + } + } + + // Recurse + if (note.renote) { + this.syncVisibility(note.renote); + } + if (note.reply) { + this.syncVisibility(note.reply); + } + } + + // Based on NoteEntityService.hideNote + private shouldRedact(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData): boolean { + // Never redact our own notes + if (user?.id === note.userId) return false; + + // Redact if sign-in required + if (note.user.requireSigninToViewContents && !user) return true; + + // Redact if note has expired + if (note.user.makeNotesHiddenBefore) { + const hiddenBefore = note.user.makeNotesHiddenBefore * 1000; + const createdAt = 'createdAt' in note + ? new Date(note.createdAt).getTime() + : this.idService.parse(note.id).date.getTime(); + + // I don't understand this logic, but I tried to break it out for readability + const hiddenOpt1 = hiddenBefore <= 0 && (Date.now() - createdAt > 0 - hiddenBefore); + const hiddenOpt2 = hiddenBefore > 0 && (createdAt < hiddenBefore); + if (hiddenOpt1 || hiddenOpt2) return true; + } + + // Redact if inaccessible. + // We have to repeat the check in case note visibility changed in treatVisibility! + if (!this.isAccessible(note, user, data)) { + return true; + } + + // Otherwise don't redact + return false; + } + + // Based on inconsistent logic from all around the app + private shouldSilence(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData, filters: NoteVisibilityFilters | undefined): boolean { + if (this.shouldSilenceForMute(note, data)) { + return true; + } + + if (this.shouldSilenceForSilence(note, user, data, filters?.includeSilencedAuthor ?? false)) { + return true; + } + + if (!filters?.includeReplies && this.shouldSilenceForFollowWithoutReplies(note, user, data)) { + return true; + } + + return false; + } + + private shouldSilenceForMute(note: PopulatedNote, data: NoteVisibilityData): boolean { + // Silence if we've muted the thread + if (data.userMutedThreads?.has(note.threadId ?? note.id)) return true; + + // Silence if we've muted the note + if (data.userMutedNotes?.has(note.id)) return true; + + // Silence if we've muted the user + if (data.userMutedUsers?.has(note.userId)) return true; + + // Silence if we've muted renotes from the user + if (isPopulatedBoost(note) && data.userMutedUserRenotes?.has(note.userId)) return true; + + // Silence if we've muted the instance + if (note.userHost && data.userMutedInstances?.has(note.userHost)) return true; + + // Otherwise don't silence + return false; + } + + private shouldSilenceForSilence(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData, includeSilencedAuthor: boolean): boolean { + // Don't silence if it's us + if (note.userId === user?.id) return false; + + // Don't silence if we're following + if (data.userFollowings?.has(note.userId)) return false; + + if (!includeSilencedAuthor) { + // Silence if user is silenced + if (note.user.isSilenced) return true; + + // Silence if user instance is silenced + if (note.user.instance?.isSilenced) return true; + } + + // Silence if renote is silenced + if (note.renote && note.renote.userId !== note.userId && this.shouldSilenceForSilence(note.renote, user, data, false)) return true; + + // Silence if reply is silenced + if (note.reply && note.reply.userId !== note.userId && this.shouldSilenceForSilence(note.reply, user, data, false)) return true; + + // Otherwise don't silence + return false; + } + + private shouldSilenceForFollowWithoutReplies(note: PopulatedNote, user: PopulatedUser, data: NoteVisibilityData): boolean { + // Don't silence if it's not a reply + if (!note.reply) return false; + + // Don't silence if it's a self-reply + if (note.reply.userId === note.userId) return false; + + // Don't silence if it's a reply to us + if (note.reply.userId === user?.id) return false; + + // Don't silence if it's our post + if (note.userId === user?.id) return false; + + // Don't silence if we follow w/ replies + if (user && data.userFollowings?.get(user.id)?.withReplies) return false; + + // Silence otherwise + return true; + } +} + +export interface NoteVisibilityData { + userBlockers: Set | null; + userFollowings: Map> | null; + userMutedThreads: Set | null; + userMutedNotes: Set | null; + userMutedUsers: Set | null; + userMutedUserRenotes: Set | null; + userMutedInstances: Set | null; +} + +export type PopulatedUser = Pick | null | undefined; + +export type PopulatedNote = PopulatedMiNote | Packed<'Note'>; + +type PopulatedMiNote = MiNote & { + user: MiUser & { + instance: MiInstance | null, + } + renote: PopulatedMiNote | null, + reply: PopulatedMiNote | null, +}; + +function isPopulatedBoost(note: PopulatedNote): note is PopulatedNote & { renote: PopulatedNote } { + return note.renoteId != null + && note.replyId == null + && note.text == null + && note.cw == null + && (note.fileIds == null || note.fileIds.length === 0); +} + +function isPackedNote(note: MiNote | Packed<'Note'>): note is Packed<'Note'> { + // Kind of a hack: determine whether it's packed by looking for property that doesn't exist in MiNote + return 'isFavorited' in note; +} diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index 6c6ecbae6a..4fa0afc71e 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -128,29 +128,44 @@ export class QueryService { } @bindThis - public generateMutedUserQueryForNotes(q: SelectQueryBuilder, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): SelectQueryBuilder { - // 投稿の作者をミュートしていない かつ - // 投稿の返信先の作者をミュートしていない かつ - // 投稿の引用元の作者をミュートしていない - return this - .andNotMutingUser(q, ':meId', 'note.userId', exclude) + public generateMutedUserQueryForNotes(q: SelectQueryBuilder, me: { id: MiUser['id'] }, excludeAuthor = false): SelectQueryBuilder { + if (!excludeAuthor) { + this + // muted user + .andNotMutingUser(q, ':meId', 'note.userId') + // muted host + .andWhere(new Brackets(qb => { + qb.orWhere('note.userHost IS NULL'); + this.orFollowingUser(qb, ':meId', 'note.userId'); + this.orNotMutingInstance(qb, ':meId', 'note.userHost'); + })); + } + + return q + // muted reply user .andWhere(new Brackets(qb => this - .orNotMutingUser(qb, ':meId', 'note.replyUserId', exclude) + .orNotMutingUser(qb, ':meId', 'note.replyUserId') + .orWhere('note.replyUserId = note.userId') .orWhere('note.replyUserId IS NULL'))) + // muted renote user .andWhere(new Brackets(qb => this - .orNotMutingUser(qb, ':meId', 'note.renoteUserId', exclude) + .orNotMutingUser(qb, ':meId', 'note.renoteUserId') + .orWhere('note.renoteUserId = note.userId') .orWhere('note.renoteUserId IS NULL'))) - // TODO exclude should also pass a host to skip these instances - // mute instances - .andWhere(new Brackets(qb => this - .andNotMutingInstance(qb, ':meId', 'note.userHost') - .orWhere('note.userHost IS NULL'))) - .andWhere(new Brackets(qb => this - .orNotMutingInstance(qb, ':meId', 'note.replyUserHost') - .orWhere('note.replyUserHost IS NULL'))) - .andWhere(new Brackets(qb => this - .orNotMutingInstance(qb, ':meId', 'note.renoteUserHost') - .orWhere('note.renoteUserHost IS NULL'))) + // muted reply host + .andWhere(new Brackets(qb => { + qb.orWhere('note.replyUserHost IS NULL'); + qb.orWhere('note.replyUserHost = note.userHost'); + this.orFollowingUser(qb, ':meId', 'note.replyUserId'); + this.orNotMutingInstance(qb, ':meId', 'note.replyUserHost'); + })) + // muted renote host + .andWhere(new Brackets(qb => { + qb.orWhere('note.renoteUserHost IS NULL'); + qb.orWhere('note.renoteUserHost = note.userHost'); + this.orFollowingUser(qb, ':meId', 'note.renoteUserId'); + this.orNotMutingInstance(qb, ':meId', 'note.renoteUserHost'); + })) .setParameters({ meId: me.id }); } @@ -238,10 +253,10 @@ export class QueryService { @bindThis public generateSilencedUserQueryForNotes(q: SelectQueryBuilder, me?: { id: MiUser['id'] } | null, excludeAuthor = false): SelectQueryBuilder { - const checkFor = (key: 'user' | 'replyUser' | 'renoteUser') => { + const checkFor = (key: 'user' | 'replyUser' | 'renoteUser', userKey: 'note.user' | 'reply.user' | 'renote.user') => { // These are de-duplicated, since most call sites already provide some of them. this.leftJoin(q, `note.${key}Instance`, `${key}Instance`); // note->instance - this.leftJoin(q, `note.${key}`, key); // note->user + this.leftJoin(q, userKey, key); // note->user q.andWhere(new Brackets(qb => { // case 1: user does not exist (note is not reply/renote) @@ -270,10 +285,10 @@ export class QueryService { } if (!excludeAuthor) { - checkFor('user'); + checkFor('user', 'note.user'); } - checkFor('replyUser'); - checkFor('renoteUser'); + checkFor('replyUser', 'reply.user'); + checkFor('renoteUser', 'renote.user'); return q; } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index bb56c6c745..478438b042 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -31,6 +31,7 @@ import { isQuote, isRenote } from '@/misc/is-renote.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js'; import { CacheService } from '@/core/CacheService.js'; +import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import type { DataSource } from 'typeorm'; const FALLBACK = '\u2764'; @@ -108,6 +109,7 @@ export class ReactionService { private notificationService: NotificationService, private perUserReactionsChart: PerUserReactionsChart, private readonly cacheService: CacheService, + private readonly noteVisibilityService: NoteVisibilityService, ) { } @@ -122,7 +124,8 @@ export class ReactionService { } // check visibility - if (!await this.noteEntityService.isVisibleForMe(note, user.id, { me: user })) { + const { accessible } = await this.noteVisibilityService.checkNoteVisibilityAsync(note, user); + if (!accessible) { throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); } diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index fefb0f59a8..6fac67f42d 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -401,6 +401,7 @@ export class WebhookTestService { text: note.text, cw: note.cw, userId: note.userId, + userHost: note.userHost ?? null, user: await this.toPackedUserLite(note.user ?? generateDummyUser()), replyId: note.replyId, renoteId: note.renoteId, diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 009d4cbd39..8aaa229466 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -38,6 +38,7 @@ import FederationChart from '@/core/chart/charts/federation.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js'; import { CacheService } from '@/core/CacheService.js'; +import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost, isActivity, IObjectWithId } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; @@ -100,6 +101,7 @@ export class ApInboxService { private readonly federationChart: FederationChart, private readonly updateInstanceQueue: UpdateInstanceQueue, private readonly cacheService: CacheService, + private readonly noteVisibilityService: NoteVisibilityService, ) { this.logger = this.apLoggerService.logger; } @@ -367,7 +369,8 @@ export class ApInboxService { const renote = await this.apNoteService.resolveNote(target, { resolver, sentFrom: getApId(target) }); if (renote == null) return 'announce target is null'; - if (!await this.noteEntityService.isVisibleForMe(renote, actor.id, { me: actor })) { + const { accessible } = await this.noteVisibilityService.checkNoteVisibilityAsync(renote, actor); + if (!accessible) { return 'skip: invalid actor for this activity'; } diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 8841c2b63d..1f5496aa93 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -17,8 +17,9 @@ import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import { QueryService } from '@/core/QueryService.js'; -import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { Config } from '@/config.js'; +import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; +import type { NoteVisibilityData } from '@/core/NoteVisibilityService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CacheService } from '../CacheService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; @@ -101,6 +102,9 @@ export class NoteEntityService implements OnModuleInit { @Inject(DI.noteFavoritesRepository) private noteFavoritesRepository: NoteFavoritesRepository, + // This is public to avoid weaving a whole new service through the Channel class hierarchy. + public readonly noteVisibilityService: NoteVisibilityService, + private readonly queryService: QueryService, //private userEntityService: UserEntityService, //private driveFileEntityService: DriveFileEntityService, @@ -121,6 +125,8 @@ export class NoteEntityService implements OnModuleInit { this.idService = this.moduleRef.get('IdService'); } + // Implementation moved to NoteVisibilityService + /* @bindThis private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] { if (packedNote.visibility === 'public' || packedNote.visibility === 'home') { @@ -136,24 +142,31 @@ export class NoteEntityService implements OnModuleInit { } return packedNote.visibility; } + */ @bindThis - public async hideNotes(notes: Packed<'Note'>[], meId: string | null): Promise { - const myFollowing = meId ? new Map(await this.cacheService.userFollowingsCache.fetch(meId)) : new Map>(); - const myBlockers = meId ? new Set(await this.cacheService.userBlockedCache.fetch(meId)) : new Set(); + public async hideNotes(notes: Packed<'Note'>[], meId: string | null, hint?: Partial): Promise { + const me = meId ? await this.cacheService.findUserById(meId) : null; + const data = await this.noteVisibilityService.populateData(me, hint); - // This shouldn't actually await, but we have to wrap it anyway because hideNote() is async - await Promise.all(notes.map(note => this.hideNote(note, meId, { - myFollowing, - myBlockers, - }))); + for (const note of notes) { + this.hideNote(note, me, data); + } } @bindThis - public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null, hint?: { - myFollowing?: ReadonlyMap> | ReadonlySet, - myBlockers?: ReadonlySet, - }): Promise { + public async hideNoteAsync(packedNote: Packed<'Note'>, me: string | Pick | null, hint?: Partial): Promise { + const { redact } = await this.noteVisibilityService.checkNoteVisibilityAsync(packedNote, me, { hint }); + + if (redact) { + this.redactNoteContents(packedNote); + } + } + + @bindThis + public hideNote(packedNote: Packed<'Note'>, me: Pick | null, data: NoteVisibilityData): void { + // Implementation moved to NoteVisibilityService + /* if (meId === packedNote.userId) return; // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) @@ -232,8 +245,17 @@ export class NoteEntityService implements OnModuleInit { if (isBlocked) hide = true; } + */ + + const hide = this.noteVisibilityService.checkNoteVisibility(packedNote, me, { data }).redact; if (hide) { + this.redactNoteContents(packedNote); + } + } + + private redactNoteContents(packedNote: Packed<'Note'>) { + { packedNote.visibleUserIds = undefined; packedNote.fileIds = []; packedNote.files = []; @@ -477,6 +499,8 @@ export class NoteEntityService implements OnModuleInit { return undefined; } + // Implementation moved to NoteVisibilityService + /* @bindThis public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null, hint?: { myFollowing?: ReadonlySet, @@ -493,7 +517,7 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - public isVisibleForMeSync(note: MiNote, me: Pick | null, myFollowings: ReadonlySet | null, myBlockers: ReadonlySet | null): boolean { + public isVisibleForMeSync(note: MiNote | Packed<'Note'>, me: Pick | null, myFollowings: ReadonlySet | null, myBlockers: ReadonlySet | null): boolean { // We can always view our own notes if (me?.id === note.userId) { return true; @@ -509,6 +533,8 @@ export class NoteEntityService implements OnModuleInit { if (note.visibility === 'specified') { if (me == null) { return false; + } else if (!note.visibleUserIds) { + return false; } else { // 指定されているかどうか return note.visibleUserIds.includes(me.id); @@ -522,9 +548,13 @@ export class NoteEntityService implements OnModuleInit { } else if (note.reply && (me.id === note.reply.userId)) { // 自分の投稿に対するリプライ return true; + } else if (!note.mentions) { + return false; } else if (note.mentions.includes(me.id)) { // 自分へのメンション return true; + } else if (!note.visibleUserIds) { + return false; } else if (note.visibleUserIds.includes(me.id)) { // Explicitly visible to me return true; @@ -533,19 +563,19 @@ export class NoteEntityService implements OnModuleInit { const following = myFollowings?.has(note.userId); const userHost = me.host; - /* If we know the following, everyhting is fine. - - But if we do not know the following, it might be that both the - author of the note and the author of the like are remote users, - in which case we can never know the following. Instead we have - to assume that the users are following each other. - */ + // If we know the following, everyhting is fine. + // + // But if we do not know the following, it might be that both the + // author of the note and the author of the like are remote users, + // in which case we can never know the following. Instead we have + // to assume that the users are following each other. return following || (note.userHost != null && userHost != null); } } return true; } + */ @bindThis public async packAttachedFiles(fileIds: MiNote['fileIds'], packedFiles: Map | null>): Promise[]> { @@ -649,6 +679,7 @@ export class NoteEntityService implements OnModuleInit { createdAt: this.idService.parse(note.id).date.toISOString(), updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined, userId: note.userId, + userHost: note.userHost, user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me), text: text, cw: note.cw, @@ -719,12 +750,14 @@ export class NoteEntityService implements OnModuleInit { } : {}), }); - this.treatVisibility(packed); + this.noteVisibilityService.syncVisibility(packed); if (!opts.skipHide) { - await this.hideNote(packed, meId, meId == null ? undefined : { - myFollowing: opts._hint_?.userFollowings.get(meId), - myBlockers: opts._hint_?.userBlockers.get(meId), + await this.hideNoteAsync(packed, meId, { + userFollowings: meId ? opts._hint_?.userFollowings.get(meId) : null, + userBlockers: meId ? opts._hint_?.userBlockers.get(meId) : null, + userMutedNotes: opts._hint_?.mutedNotes, + userMutedThreads: opts._hint_?.mutedThreads, }); } diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 6d3b9fe15e..79b36ba0da 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -50,6 +50,10 @@ export const packedNoteSchema = { optional: false, nullable: false, format: 'id', }, + userHost: { + type: 'string', + optional: false, nullable: true, + }, user: { type: 'object', ref: 'UserLite', diff --git a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts index e31b9be702..f5236e9423 100644 --- a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts @@ -93,7 +93,7 @@ export default class extends Endpoint { // eslint- .leftJoin('(select "host" from "instance" where "isBubbled" = true)', 'bubbleInstance', '"bubbleInstance"."host" = "note"."userHost"') .andWhere('"bubbleInstance" IS NOT NULL'); this.queryService - .leftJoin(query, 'note.userInstance', 'userInstance', '"userInstance"."host" = "bubbleInstance"."host"'); + .leftJoin(query, 'note.userInstance', 'userInstance'); this.queryService.generateBlockedHostQueryForNote(query); this.queryService.generateSilencedUserQueryForNotes(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 42dbf33d0d..d6fccd1b84 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -18,6 +18,7 @@ import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -261,6 +262,7 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private noteCreateService: NoteCreateService, + private readonly noteVisibilityService: NoteVisibilityService, ) { super(meta, paramDef, async (ps, me) => { if (ps.text && ps.text.length > this.config.maxNoteLength) { @@ -303,7 +305,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchRenoteTarget); } else if (isRenote(renote) && !isQuote(renote)) { throw new ApiError(meta.errors.cannotReRenote); - } else if (!await this.noteEntityService.isVisibleForMe(renote, me.id)) { + } else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(renote, me)).accessible) { throw new ApiError(meta.errors.cannotRenoteDueToVisibility); } @@ -351,7 +353,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchReplyTarget); } else if (isRenote(reply) && !isQuote(reply)) { throw new ApiError(meta.errors.cannotReplyToPureRenote); - } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id, { me })) { + } else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(reply, me)).accessible) { throw new ApiError(meta.errors.cannotReplyToInvisibleNote); } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') { throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 2689451a73..717dab59e1 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -17,6 +17,7 @@ import { NoteEditService } from '@/core/NoteEditService.js'; import { DI } from '@/di-symbols.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -311,6 +312,7 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private noteEditService: NoteEditService, + private readonly noteVisibilityService: NoteVisibilityService, ) { super(meta, paramDef, async (ps, me) => { if (ps.text && ps.text.length > this.config.maxNoteLength) { @@ -408,7 +410,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchReplyTarget); } else if (isRenote(reply) && !isQuote(reply)) { throw new ApiError(meta.errors.cannotReplyToPureRenote); - } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id, { me })) { + } else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(reply, me)).accessible) { throw new ApiError(meta.errors.cannotReplyToInvisibleNote); } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') { throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 5ebd5ef362..f8c29b60d4 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -15,6 +15,7 @@ import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { hasText } from '@/models/Note.js'; import { ApiLoggerService } from '@/server/api/ApiLoggerService.js'; +import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -84,6 +85,7 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, private readonly cacheService: CacheService, private readonly loggerService: ApiLoggerService, + private readonly noteVisibilityService: NoteVisibilityService, ) { super(meta, paramDef, async (ps, me) => { const note = await this.getterService.getNote(ps.noteId).catch(err => { @@ -91,7 +93,8 @@ export default class extends Endpoint { // eslint- throw err; }); - if (!(await this.noteEntityService.isVisibleForMe(note, me?.id ?? null, { me }))) { + const { accessible } = await this.noteVisibilityService.checkNoteVisibilityAsync(note, me); + if (!accessible) { throw new ApiError(meta.errors.cannotTranslateInvisibleNote); } diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 4db16ad7a0..d0363c83f8 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -220,7 +220,7 @@ export default class extends Endpoint { // eslint- this.queryService.generateSuspendedUserQueryForNote(query, true); this.queryService.generateSilencedUserQueryForNotes(query, me, true); if (me) { - this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId }); + this.queryService.generateMutedUserQueryForNotes(query, me, true); this.queryService.generateBlockedUserQueryForNotes(query, me); } diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 7e0e3e0bc6..71845125ad 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -12,6 +12,7 @@ import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { deepClone } from '@/misc/clone.js'; import type Connection from '@/server/api/stream/Connection.js'; +import { NoteVisibilityFilters } from '@/core/NoteVisibilityService.js'; /** * Stream channel @@ -26,6 +27,10 @@ export default abstract class Channel { public static readonly requireCredential: boolean; public static readonly kind?: string | null; + protected get noteVisibilityService() { + return this.noteEntityService.noteVisibilityService; + } + protected get user() { return this.connection.user; } @@ -105,8 +110,14 @@ export default abstract class Channel { return this.connection.myRecentFavorites; } + protected async checkNoteVisibility(note: Packed<'Note'>, filters?: NoteVisibilityFilters) { + // Don't use any of the local cached data, because this does everything through CacheService which is just as fast with updated data. + return await this.noteVisibilityService.checkNoteVisibilityAsync(note, this.user, { filters }); + } + /** * Checks if a note is visible to the current user *excluding* blocks and mutes. + * @deprecated use isNoteHidden instead */ protected isNoteVisibleToMe(note: Packed<'Note'>): boolean { if (note.visibility === 'public') return true; @@ -120,8 +131,9 @@ export default abstract class Channel { return note.visibleUserIds.includes(this.user.id); } - /* + /** * ミュートとブロックされてるを処理する + * @deprecated use isNoteHidden instead */ protected isNoteMutedOrBlocked(note: Packed<'Note'>): boolean { // Ignore notes that require sign-in @@ -196,12 +208,11 @@ export default abstract class Channel { // If we didn't clone the notes here, different connections would asynchronously write // different values to the same object, resulting in a random value being sent to each frontend. -- Dakkar const clonedNote = deepClone(note); - const notes = crawl(clonedNote); // Hide notes before everything else, since this modifies fields that the other functions will check. - await this.noteEntityService.hideNotes(notes, this.user.id); + const notes = crawl(clonedNote); - const [myReactions, myRenotes, myFavorites, myThreadMutings, myNoteMutings] = await Promise.all([ + const [myReactions, myRenotes, myFavorites, myThreadMutings, myNoteMutings, myFollowings] = await Promise.all([ this.noteEntityService.populateMyReactions(notes, this.user.id, { myReactions: this.myRecentReactions, }), @@ -213,13 +224,25 @@ export default abstract class Channel { }), this.noteEntityService.populateMyTheadMutings(notes, this.user.id), this.noteEntityService.populateMyNoteMutings(notes, this.user.id), + this.cacheService.userFollowingsCache.fetch(this.user.id), ]); - note.myReaction = myReactions.get(note.id) ?? null; - note.isRenoted = myRenotes.has(note.id); - note.isFavorited = myFavorites.has(note.id); - note.isMutingThread = myThreadMutings.has(note.id); - note.isMutingNote = myNoteMutings.has(note.id); + for (const n of notes) { + // Sync visibility in case there's something like "makeNotesFollowersOnlyBefore" enabled + this.noteVisibilityService.syncVisibility(n); + + n.myReaction = myReactions.get(n.id) ?? null; + n.isRenoted = myRenotes.has(n.id); + n.isFavorited = myFavorites.has(n.id); + n.isMutingThread = myThreadMutings.has(n.id); + n.isMutingNote = myNoteMutings.has(n.id); + n.user.bypassSilence = n.userId === this.user.id || myFollowings.has(n.userId); + } + + // Hide notes *after* we sync visibility + await this.noteEntityService.hideNotes(notes, this.user.id, { + userFollowings: myFollowings, + }); return clonedNote; } diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 0974dbdb25..20aa934468 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -41,7 +41,8 @@ class AntennaChannel extends Channel { if (data.type === 'note') { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true }); - if (this.isNoteMutedOrBlocked(note)) return; + const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true }); + if (!accessible || silence) return; this.send('note', note); } else { diff --git a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts index a7f538eeda..6adb861ff3 100644 --- a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts @@ -8,9 +8,9 @@ import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import Channel, { MiChannelService } from '../channel.js'; class BubbleTimelineChannel extends Channel { @@ -47,8 +47,6 @@ class BubbleTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { - const isMe = this.user?.id === note.userId; - if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; if (!this.withBots && note.user.isBot) return; @@ -56,27 +54,9 @@ class BubbleTimelineChannel extends Channel { if (note.channelId != null) return; if (!this.utilityService.isBubbledHost(note.user.host)) return; - if (this.isNoteMutedOrBlocked(note)) return; - - if (note.reply) { - const reply = note.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (!this.isNoteVisibleToMe(reply)) return; - if (!this.following.get(note.userId)?.withReplies) { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return; - } - } - - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (!this.isNoteVisibleToMe(reply)) return; - } - } + const { accessible, silence } = await this.checkNoteVisibility(note); + if (!accessible || silence) return; + if (!this.withRenotes && isPackedPureRenote(note)) return; const clonedNote = await this.rePackNote(note); this.send('note', clonedNote); diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 9eea423088..08d9c59c83 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import type { JsonObject } from '@/misc/json-value.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import Channel, { type MiChannelService } from '../channel.js'; class ChannelChannel extends Channel { @@ -45,9 +46,9 @@ class ChannelChannel extends Channel { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; - - if (this.isNoteMutedOrBlocked(note)) return; + const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true }); + if (!accessible || silence) return; + if (!this.withRenotes && isPackedPureRenote(note)) return; const clonedNote = await this.rePackNote(note); this.send('note', clonedNote); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 9bdd299ddb..8e6fa0c4ea 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -48,36 +48,15 @@ class GlobalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { - const isMe = this.user?.id === note.userId; - if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; if (!this.withBots && note.user.isBot) return; if (note.visibility !== 'public') return; if (note.channelId != null) return; - if (this.isNoteMutedOrBlocked(note)) return; - if (!this.isNoteVisibleToMe(note)) return; - - if (note.reply) { - const reply = note.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (!this.isNoteVisibleToMe(reply)) return; - if (!this.following.get(note.userId)?.withReplies) { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return; - } - } - - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (!this.isNoteVisibleToMe(reply)) return; - } - } + const { accessible, silence } = await this.checkNoteVisibility(note); + if (!accessible || silence) return; + if (!this.withRenotes && isPackedPureRenote(note)) return; const clonedNote = await this.rePackNote(note); this.send('note', clonedNote); diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index e0331828d2..953c296c11 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -43,7 +43,8 @@ class HashtagChannel extends Channel { const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag)))); if (!matched) return; - if (this.isNoteMutedOrBlocked(note)) return; + const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true }); + if (!accessible || silence) return; const clonedNote = await this.rePackNote(note); this.send('note', clonedNote); diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index bb28cbf81e..6e7936c3e0 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -7,7 +7,7 @@ import { Injectable } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -50,28 +50,9 @@ class HomeTimelineChannel extends Channel { if (!isMe && !this.following.has(note.userId)) return; } - if (this.isNoteMutedOrBlocked(note)) return; - if (!this.isNoteVisibleToMe(note)) return; - - if (note.reply) { - const reply = note.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (!this.isNoteVisibleToMe(reply)) return; - if (!this.following.get(note.userId)?.withReplies) { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; - } - } - - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (!this.isNoteVisibleToMe(reply)) return; - } - } + const { accessible, silence } = await this.checkNoteVisibility(note); + if (!accessible || silence) return; + if (!this.withRenotes && isPackedPureRenote(note)) return; const clonedNote = await this.rePackNote(note); this.send('note', clonedNote); diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 82d96c6e7e..e5576e9102 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -67,28 +67,10 @@ class HybridTimelineChannel extends Channel { (note.channelId != null && this.followingChannels.has(note.channelId)) )) return; - if (this.isNoteMutedOrBlocked(note)) return; - if (!this.isNoteVisibleToMe(note)) return; - - if (note.reply) { - const reply = note.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (!this.isNoteVisibleToMe(reply)) return; - if (!this.following.get(note.userId)?.withReplies && !this.withReplies) { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; - } - } - - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (!this.isNoteVisibleToMe(reply)) return; - } - } + const { accessible, silence } = await this.checkNoteVisibility(note); + if (!accessible || silence) return; + if (!this.withRenotes && isPackedPureRenote(note)) return; + if (!this.withReplies && note.replyId != null) return; const clonedNote = await this.rePackNote(note); this.send('note', clonedNote); diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index c6c04d356f..9d76419c6b 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -50,8 +50,6 @@ class LocalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { - const isMe = this.user?.id === note.userId; - if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; if (!this.withBots && note.user.isBot) return; @@ -59,28 +57,10 @@ class LocalTimelineChannel extends Channel { if (note.visibility !== 'public') return; if (note.channelId != null) return; - if (this.isNoteMutedOrBlocked(note)) return; - if (!this.isNoteVisibleToMe(note)) return; - - // 関係ない返信は除外 - if (note.reply) { - const reply = note.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (!this.isNoteVisibleToMe(reply)) return; - if (!this.following.get(note.userId)?.withReplies) { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return; - } - } - - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (!this.isNoteVisibleToMe(reply)) return; - } - } + const { accessible, silence } = await this.checkNoteVisibility(note); + if (!accessible || silence) return; + if (!this.withRenotes && isPackedPureRenote(note)) return; + if (!this.withReplies && note.replyId != null) return; const clonedNote = await this.rePackNote(note); this.send('note', clonedNote); diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 193907504a..30f18053f2 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -35,24 +35,19 @@ class MainChannel extends Channel { if (isUserFromMutedInstance(data.body, this.userMutedInstances)) return; if (data.body.userId && this.userIdsWhoMeMuting.has(data.body.userId)) return; - if (data.body.note && data.body.note.isHidden) { - if (this.isNoteMutedOrBlocked(data.body.note)) return; - if (!this.isNoteVisibleToMe(data.body.id)) return; - const note = await this.noteEntityService.pack(data.body.note.id, this.user, { - detail: true, - }); - data.body.note = note; + if (data.body.note) { + const { accessible, silence } = await this.checkNoteVisibility(data.body.note, { includeReplies: true }); + if (!accessible || silence) return; + + data.body.note = await this.rePackNote(data.body.note); } break; } case 'mention': { - if (this.isNoteMutedOrBlocked(data.body)) return; - if (data.body.isHidden) { - const note = await this.noteEntityService.pack(data.body.id, this.user, { - detail: true, - }); - data.body = note; - } + const { accessible, silence } = await this.checkNoteVisibility(data.body, { includeReplies: true }); + if (!accessible || silence) return; + + data.body = await this.rePackNote(data.body); break; } } diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index 2a8d4f7ffd..9c86734285 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { JsonObject } from '@/misc/json-value.js'; -import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; +import { isPackedPureRenote, isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; import Channel, { type MiChannelService } from '../channel.js'; class RoleTimelineChannel extends Channel { @@ -49,26 +49,8 @@ class RoleTimelineChannel extends Channel { } if (note.visibility !== 'public') return; - if (this.isNoteMutedOrBlocked(note)) return; - - if (note.reply) { - const reply = note.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (!this.isNoteVisibleToMe(reply)) return; - if (!this.following.get(note.userId)?.withReplies) { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return; - } - } - - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (!this.isNoteVisibleToMe(reply)) return; - } - } + const { accessible, silence } = await this.checkNoteVisibility(note); + if (!accessible || silence) return; const clonedNote = await this.rePackNote(note); this.send('note', clonedNote); diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 98bcbd11ba..18dbd40902 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -9,7 +9,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -82,8 +82,6 @@ class UserListChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { - const isMe = this.user?.id === note.userId; - // チャンネル投稿は無視する if (note.channelId) return; @@ -91,28 +89,9 @@ class UserListChannel extends Channel { if (!Object.hasOwn(this.membershipsMap, note.userId)) return; - if (this.isNoteMutedOrBlocked(note)) return; - if (!this.isNoteVisibleToMe(note)) return; - - if (note.reply) { - const reply = note.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (!this.isNoteVisibleToMe(reply)) return; - if (!this.following.get(note.userId)?.withReplies) { - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; - } - } - - // 純粋なリノート(引用リノートでないリノート)の場合 - if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { - if (!this.withRenotes) return; - if (note.renote.reply) { - const reply = note.renote.reply; - // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (!this.isNoteVisibleToMe(reply)) return; - } - } + const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true }); + if (!accessible || silence) return; + if (!this.withRenotes && isPackedPureRenote(note)) return; const clonedNote = await this.rePackNote(note); this.send('note', clonedNote); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08d5ef8445..faf9725c3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1508,8 +1508,8 @@ packages: '@apidevtools/swagger-methods@3.0.2': resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} - '@asamuzakjp/css-color@3.1.1': - resolution: {integrity: sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} @@ -1965,28 +1965,28 @@ packages: resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} - '@csstools/css-calc@2.1.2': - resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==} + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-color-parser@3.0.8': - resolution: {integrity: sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==} + '@csstools/css-color-parser@3.0.10': + resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-parser-algorithms@3.0.4': - resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-tokenizer@3.0.3': - resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} '@cypress/request@3.0.8': @@ -2168,6 +2168,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3099,6 +3105,7 @@ packages: '@readme/json-schema-ref-parser@1.2.0': resolution: {integrity: sha512-Bt3QVovFSua4QmHa65EHUmh2xS0XJ3rgTEUPH998f4OW4VVJke3BuS16f+kM0ZLOGdvIrzrPRqwihuv5BAjtrA==} + deprecated: This package is no longer maintained. Please use `@apidevtools/json-schema-ref-parser` instead. '@readme/openapi-parser@2.7.0': resolution: {integrity: sha512-P8WSr8WTOxilnT89tcCRKWYsG/II4sAwt1a/DIWub8xTtkrG9cCBBy/IUcvc5X8oGWN82MwcTA3uEkDrXZd/7A==} @@ -4221,8 +4228,8 @@ packages: '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} '@types/proxy-addr@2.0.3': resolution: {integrity: sha512-TgAHHO4tNG3HgLTUhB+hM4iwW6JUNeQHCLnF1DjaDA9c69PN+IasoFu2MYDhubFc+ZIw5c5t9DMtjvrD6R3Egg==} @@ -4266,8 +4273,8 @@ packages: '@types/sanitize-html@2.15.0': resolution: {integrity: sha512-71Z6PbYsVKfp4i6Jvr37s5ql6if1Q/iJQT80NbaSi7uGaG8CqBMXP0pk/EsURAOuGdk5IJCd/vnzKrR7S3Txsw==} - '@types/scheduler@0.23.0': - resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==} + '@types/scheduler@0.26.0': + resolution: {integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==} '@types/seedrandom@3.0.8': resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==} @@ -4365,10 +4372,26 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/project-service@8.39.1': + resolution: {integrity: sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@8.31.0': resolution: {integrity: sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.39.1': + resolution: {integrity: sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.39.1': + resolution: {integrity: sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.31.0': resolution: {integrity: sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4380,12 +4403,22 @@ packages: resolution: {integrity: sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.39.1': + resolution: {integrity: sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.31.0': resolution: {integrity: sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/typescript-estree@8.39.1': + resolution: {integrity: sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.31.0': resolution: {integrity: sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4393,10 +4426,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/utils@8.39.1': + resolution: {integrity: sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@8.31.0': resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.39.1': + resolution: {integrity: sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -4649,6 +4693,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.0: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} @@ -5538,8 +5587,8 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - cssstyle@4.3.0: - resolution: {integrity: sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} csstype@3.1.3: @@ -5630,6 +5679,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -5638,8 +5696,8 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - decimal.js@10.5.0: - resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} decode-bmp@0.2.1: resolution: {integrity: sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==} @@ -6056,6 +6114,10 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@9.25.1: resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6070,6 +6132,10 @@ packages: resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -6354,6 +6420,7 @@ packages: fluent-ffmpeg@2.1.3: resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} engines: {node: '>=18'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} @@ -6538,10 +6605,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} @@ -6827,6 +6896,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -7494,6 +7564,7 @@ packages: lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} @@ -7975,6 +8046,7 @@ packages: multer@1.4.5-lts.2: resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==} engines: {node: '>= 6.0.0'} + deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version. mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} @@ -8055,6 +8127,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -8156,8 +8229,8 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nwsapi@2.2.19: - resolution: {integrity: sha512-94bcyI3RsqiZufXjkr3ltkI86iEl+I7uiHVDtcq9wJUTwYQJ5odHDeSzkkrRzi80jJ8MaeZgqKjH1bAWAFw9bA==} + nwsapi@2.2.21: + resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} oauth@0.10.2: resolution: {integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==} @@ -8466,6 +8539,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pid-port@1.0.2: resolution: {integrity: sha512-Khqp07zX8IJpmIg56bHrLxS3M0iSL4cq6wnMq8YE7r/hSw3Kn4QxYS6QJg8Bs22Z7CSVj7eSsxFuigYVIFWmjg==} engines: {node: '>=18'} @@ -9745,10 +9822,12 @@ packages: superagent@9.0.2: resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} engines: {node: '>=14.18.0'} + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net supertest@7.1.0: resolution: {integrity: sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==} engines: {node: '>=14.18.0'} + deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -9880,10 +9959,17 @@ packages: tldts-core@6.1.63: resolution: {integrity: sha512-H1XCt54xY+QPbwhTgmxLkepX0MVHu3USfMmejiCOdkMbRcP22Pn2FVF127r/GWXVDmXTRezyF3Ckvhn4Fs6j7Q==} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts@6.1.63: resolution: {integrity: sha512-YWwhsjyn9sB/1rOkSRYxvkN/wl5LFM1QDv6F2pVR+pb/jFne4EOBxHfkKVWvDIBEAw9iGOwwubHtQTm0WRT5sQ==} hasBin: true + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tmp@0.2.3: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} @@ -9936,8 +10022,8 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@5.1.0: - resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} tree-kill@1.2.2: @@ -10456,6 +10542,9 @@ packages: vue-component-type-helpers@2.2.10: resolution: {integrity: sha512-iDUO7uQK+Sab2tYuiP9D1oLujCWlhHELHMgV/cB13cuGbG4qwkLHvtfWb6FzvxrIOPDnU0oHsz2MlQjhYDeaHA==} + vue-component-type-helpers@3.0.5: + resolution: {integrity: sha512-uoNZaJ+a1/zppa/Vgmi8zIOP2PHXDN2rT8NyF+zQRK6ZG94lNB9prcV0GdLJbY9i9lrD47JOVIH92SaiA7oJ1A==} + vue-demi@0.14.7: resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} engines: {node: '>=12'} @@ -10654,6 +10743,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xev@3.0.2: resolution: {integrity: sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw==} @@ -10763,12 +10864,12 @@ snapshots: '@apidevtools/swagger-methods@3.0.2': {} - '@asamuzakjp/css-color@3.1.1': + '@asamuzakjp/css-color@3.2.0': dependencies: - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 optional: true @@ -11268,7 +11369,7 @@ snapshots: '@babel/traverse': 7.24.7 '@babel/types': 7.27.1 convert-source-map: 2.0.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -11288,7 +11389,7 @@ snapshots: '@babel/traverse': 7.24.7 '@babel/types': 7.25.7 convert-source-map: 2.0.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -11521,7 +11622,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.27.2 '@babel/types': 7.25.7 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -11636,26 +11737,26 @@ snapshots: '@csstools/color-helpers@5.0.2': optional: true - '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 optional: true - '@csstools/css-color-parser@3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/color-helpers': 5.0.2 - '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 optional: true - '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-tokenizer': 3.0.4 optional: true - '@csstools/css-tokenizer@3.0.3': + '@csstools/css-tokenizer@3.0.4': optional: true '@cypress/request@3.0.8': @@ -11793,6 +11894,11 @@ snapshots: eslint: 9.25.1 eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.7.0(eslint@9.25.1)': + dependencies: + eslint: 9.25.1 + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} '@eslint/compat@1.1.1': {} @@ -11800,7 +11906,7 @@ snapshots: '@eslint/config-array@0.20.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -11814,7 +11920,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -13905,16 +14011,16 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.5.14(typescript@5.8.3) - vue-component-type-helpers: 2.2.10 + vue-component-type-helpers: 3.0.5 '@stylistic/eslint-plugin@4.2.0(eslint@9.25.1)(typescript@5.8.3)': dependencies: - '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.39.1(eslint@9.25.1)(typescript@5.8.3) eslint: 9.25.1 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.2 + picomatch: 4.0.3 transitivePeerDependencies: - supports-color - typescript @@ -14065,7 +14171,7 @@ snapshots: '@tokenizer/inflate@0.2.7': dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) fflate: 0.8.2 token-types: 6.0.0 transitivePeerDependencies: @@ -14306,7 +14412,7 @@ snapshots: pg-protocol: 1.8.0 pg-types: 2.2.0 - '@types/prop-types@15.7.14': {} + '@types/prop-types@15.7.15': {} '@types/proxy-addr@2.0.3': dependencies: @@ -14330,8 +14436,8 @@ snapshots: '@types/react@18.0.28': dependencies: - '@types/prop-types': 15.7.14 - '@types/scheduler': 0.23.0 + '@types/prop-types': 15.7.15 + '@types/scheduler': 0.26.0 csstype: 3.1.3 '@types/readdir-glob@1.1.1': @@ -14348,7 +14454,7 @@ snapshots: dependencies: htmlparser2: 8.0.1 - '@types/scheduler@0.23.0': {} + '@types/scheduler@0.26.0': {} '@types/seedrandom@3.0.8': {} @@ -14455,22 +14561,40 @@ snapshots: '@typescript-eslint/types': 8.31.0 '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.31.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) eslint: 9.25.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.39.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.8.3) + '@typescript-eslint/types': 8.39.1 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.31.0': dependencies: '@typescript-eslint/types': 8.31.0 '@typescript-eslint/visitor-keys': 8.31.0 + '@typescript-eslint/scope-manager@8.39.1': + dependencies: + '@typescript-eslint/types': 8.39.1 + '@typescript-eslint/visitor-keys': 8.39.1 + + '@typescript-eslint/tsconfig-utils@8.39.1(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + '@typescript-eslint/type-utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) eslint: 9.25.1 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -14479,11 +14603,13 @@ snapshots: '@typescript-eslint/types@8.31.0': {} + '@typescript-eslint/types@8.39.1': {} + '@typescript-eslint/typescript-estree@8.31.0(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.31.0 '@typescript-eslint/visitor-keys': 8.31.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14493,6 +14619,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.39.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.39.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.39.1(typescript@5.8.3) + '@typescript-eslint/types': 8.39.1 + '@typescript-eslint/visitor-keys': 8.39.1 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1) @@ -14504,11 +14646,27 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.39.1(eslint@9.25.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.25.1) + '@typescript-eslint/scope-manager': 8.39.1 + '@typescript-eslint/types': 8.39.1 + '@typescript-eslint/typescript-estree': 8.39.1(typescript@5.8.3) + eslint: 9.25.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.31.0': dependencies: '@typescript-eslint/types': 8.31.0 eslint-visitor-keys: 4.2.0 + '@typescript-eslint/visitor-keys@8.39.1': + dependencies: + '@typescript-eslint/types': 8.39.1 + eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.2.0': {} '@vitejs/plugin-vue@5.2.3(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.14(typescript@5.8.3))': @@ -14520,7 +14678,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -14874,13 +15032,19 @@ snapshots: dependencies: acorn: 8.14.1 + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn@7.4.1: {} acorn@8.14.1: {} + acorn@8.15.0: {} + agent-base@7.1.0: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -15292,7 +15456,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -15914,9 +16078,9 @@ snapshots: dependencies: css-tree: 2.2.1 - cssstyle@4.3.0: + cssstyle@4.6.0: dependencies: - '@asamuzakjp/css-color': 3.1.1 + '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 optional: true @@ -16038,6 +16202,11 @@ snapshots: ms: 2.1.3 optionalDependencies: supports-color: 8.1.1 + optional: true + + debug@4.4.1: + dependencies: + ms: 2.1.3 decamelize-keys@1.1.1: dependencies: @@ -16046,7 +16215,7 @@ snapshots: decamelize@1.2.0: {} - decimal.js@10.5.0: + decimal.js@10.6.0: optional: true decode-bmp@0.2.1: @@ -16409,7 +16578,7 @@ snapshots: esbuild-register@3.5.0(esbuild@0.25.3): dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) esbuild: 0.25.3 transitivePeerDependencies: - supports-color @@ -16540,6 +16709,8 @@ snapshots: eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} + eslint@9.25.1: dependencies: '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1) @@ -16558,7 +16729,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 @@ -16586,6 +16757,12 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} esquery@1.6.0: @@ -16758,7 +16935,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -16965,7 +17142,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -17019,7 +17196,7 @@ snapshots: follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) for-each@0.3.3: dependencies: @@ -17434,7 +17611,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -17462,7 +17639,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -17564,7 +17741,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -17807,7 +17984,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -17816,7 +17993,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -18215,14 +18392,14 @@ snapshots: jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: - cssstyle: 4.3.0 + cssstyle: 4.6.0 data-urls: 5.0.0 - decimal.js: 10.5.0 + decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.19 + nwsapi: 2.2.21 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -18233,7 +18410,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -18845,7 +19022,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 @@ -19229,7 +19406,7 @@ snapshots: dependencies: boolbase: 1.0.0 - nwsapi@2.2.19: + nwsapi@2.2.21: optional: true oauth@0.10.2: {} @@ -19522,6 +19699,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pid-port@1.0.2: dependencies: execa: 8.0.1 @@ -20211,7 +20390,7 @@ snapshots: require-in-the-middle@7.3.0: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) module-details-from-path: 1.0.3 resolve: 1.22.8 transitivePeerDependencies: @@ -20291,7 +20470,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -20411,7 +20590,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -20673,7 +20852,7 @@ snapshots: socks-proxy-agent@8.0.2: dependencies: agent-base: 7.1.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -20783,7 +20962,7 @@ snapshots: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 @@ -20960,7 +21139,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) fast-safe-stringify: 2.1.1 form-data: 4.0.2 formidable: 3.5.4 @@ -21120,11 +21299,19 @@ snapshots: tldts-core@6.1.63: optional: true + tldts-core@6.1.86: + optional: true + tldts@6.1.63: dependencies: tldts-core: 6.1.63 optional: true + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + optional: true + tmp@0.2.3: {} tmpl@1.0.5: {} @@ -21166,12 +21353,12 @@ snapshots: tough-cookie@5.1.2: dependencies: - tldts: 6.1.63 + tldts: 6.1.86 optional: true tr46@0.0.3: {} - tr46@5.1.0: + tr46@5.1.1: dependencies: punycode: 2.3.1 optional: true @@ -21340,7 +21527,7 @@ snapshots: app-root-path: 3.1.0 buffer: 6.0.3 dayjs: 1.11.13 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) dotenv: 16.5.0 glob: 10.4.5 reflect-metadata: 0.2.2 @@ -21540,7 +21727,7 @@ snapshots: vite-node@3.1.2(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3): dependencies: cac: 6.7.14 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) es-module-lexer: 1.6.0 pathe: 2.0.3 vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3) @@ -21589,7 +21776,7 @@ snapshots: '@vitest/spy': 3.1.2 '@vitest/utils': 3.1.2 chai: 5.2.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 @@ -21661,6 +21848,8 @@ snapshots: vue-component-type-helpers@2.2.10: {} + vue-component-type-helpers@3.0.5: {} + vue-demi@0.14.7(vue@3.5.14(typescript@5.8.3)): dependencies: vue: 3.5.14(typescript@5.8.3) @@ -21682,7 +21871,7 @@ snapshots: vue-eslint-parser@10.1.3(eslint@9.25.1): dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0(supports-color@5.5.0) eslint: 9.25.1 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 @@ -21795,7 +21984,7 @@ snapshots: whatwg-url@14.2.0: dependencies: - tr46: 5.1.0 + tr46: 5.1.1 webidl-conversions: 7.0.0 optional: true @@ -21893,6 +22082,12 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 6.0.5 + ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 6.0.5 + optional: true + xev@3.0.2: {} xml-js@1.6.11: