diff --git a/apps/bot/package.json b/apps/bot/package.json index d7e69ab..7b0810f 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -37,7 +37,7 @@ "ms": "^2.1.3", "sharp": "^0.35.5", "syllable-count-english": "^1.0.6", - "undici": "^8.11.2", + "undici": "^8.12.2", "zod": "^4.6.5" }, "devDependencies": { diff --git a/apps/bot/src/commands/utility/reminder/reminder.ts b/apps/bot/src/commands/utility/reminder/reminder.ts index c4cfd65..b40f864 100644 --- a/apps/bot/src/commands/utility/reminder/reminder.ts +++ b/apps/bot/src/commands/utility/reminder/reminder.ts @@ -143,7 +143,7 @@ export class ReminderCommand extends Command { } await removeReminder(id); - await unscheduleReminder(id); + await unscheduleReminder(reminder); await interaction.reply({ embeds: [successEmbed(`${emojis.rightArrow2} Reminder removed.`)], flags: MessageFlags.Ephemeral, diff --git a/apps/bot/src/interaction-handlers/reminder/snoozeHandler.ts b/apps/bot/src/interaction-handlers/reminder/snoozeHandler.ts new file mode 100644 index 0000000..e02672a --- /dev/null +++ b/apps/bot/src/interaction-handlers/reminder/snoozeHandler.ts @@ -0,0 +1,119 @@ +// QuestBot: A free and open-source Discord Bot. +// Copyright(C) 2026 Vantern +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework'; +import { + type ButtonInteraction, + LabelBuilder, + MessageFlags, + ModalBuilder, + StringSelectMenuBuilder, + TextInputBuilder, + TextInputStyle, +} from 'discord.js'; +import ms, { type StringValue } from 'ms'; +import { scheduleReminder } from '#lib/reminderScheduler.js'; +import { snoozeReminder } from '#lib/reminders.js'; +import { emojis } from '#utils/emoji.js'; + +const SNOOZE_PRESETS = [ + { label: '5 minutes', value: '5m' }, + { label: '15 minutes', value: '15m' }, + { label: '1 hour', value: '1h' }, + { label: '3 hours', value: '3h' }, + { label: '1 day', value: '1d' }, +]; + +interface SnoozeTarget { + reminderId: string; + userId: string; +} + +export class ReminderSnoozeHandler extends InteractionHandler { + public constructor(ctx: InteractionHandler.LoaderContext, options: InteractionHandler.Options) { + super(ctx, { + ...options, + interactionHandlerType: InteractionHandlerTypes.Button, + }); + } + + public override parse(interaction: ButtonInteraction) { + const [action, reminderId, userId] = interaction.customId.split(':'); + if (action !== 'reminder-snooze') return this.none(); + + return this.some({ reminderId, userId }); + } + + public async run(interaction: ButtonInteraction, { reminderId, userId }: SnoozeTarget) { + if (interaction.user.id !== userId) { + await interaction.reply({ + content: `${emojis.rightArrow2} You can't snooze others' reminders.`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + const modalId = `reminder-snooze-modal:${interaction.id}`; + await interaction.showModal( + new ModalBuilder() + .setCustomId(modalId) + .setTitle('Snooze reminder') + .addLabelComponents( + new LabelBuilder() + .setLabel('Snooze for') + .setStringSelectMenuComponent( + new StringSelectMenuBuilder().setCustomId('duration').setRequired(false).addOptions(SNOOZE_PRESETS), + ), + new LabelBuilder() + .setLabel('Or a custom duration (max 7d)') + .setTextInputComponent( + new TextInputBuilder() + .setCustomId('custom') + .setStyle(TextInputStyle.Short) + .setPlaceholder('2.5h') + .setMaxLength(20) + .setRequired(false), + ), + ), + ); + + const submitted = await interaction + .awaitModalSubmit({ filter: (m) => m.customId === modalId, time: 120_000 }) + .catch(() => null); + if (!submitted?.isFromMessage()) return; + + const custom = submitted.fields.getTextInputValue('custom').trim(); + const [preset] = submitted.fields.getStringSelectValues('duration'); + const input = custom || preset; + const duration = input ? ms(input as StringValue) : undefined; + + // 7d max duration + if (!duration || duration < 0 || duration > 604_800_000) { + await submitted.reply({ + content: `${emojis.rightArrow2} Pick a preset or enter a duration up to 7d.`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + const remindAt = new Date(Date.now() + duration); + const reminder = await snoozeReminder(reminderId, userId, remindAt); + if (!reminder) { + await submitted.update({ components: [] }); + await submitted.followUp({ + content: `${emojis.rightArrow2} This reminder can no longer be snoozed.`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + await scheduleReminder(reminder); + + const unix = Math.floor(remindAt.getTime() / 1000); + await submitted.update({ + content: `${submitted.message.content}\n-# snoozed, reminding you again `, + components: [], + }); + } +} diff --git a/apps/bot/src/lib/purgeScheduler.ts b/apps/bot/src/lib/purgeScheduler.ts index 243ee18..2d0dd5f 100644 --- a/apps/bot/src/lib/purgeScheduler.ts +++ b/apps/bot/src/lib/purgeScheduler.ts @@ -4,6 +4,7 @@ import { logger } from './logger.js'; import { createQueue } from './queue.js'; +import { clearExpiredReminders } from './reminders.js'; import { purgeDeletedServers } from './servers.js'; import { purgeExpiredWarns } from './warns.js'; @@ -11,9 +12,10 @@ export function initPurgeScheduler(): void { const queue = createQueue('purge', async () => { await purgeExpiredWarns(); await purgeDeletedServers(); + await clearExpiredReminders(); }); queue - .upsertJobScheduler('daily-purge', { pattern: '0 3 * * *' }, { name: 'purge' }) + .upsertJobScheduler('daily-purge', { pattern: '0 3 * * *' }, { name: 'purge' }) // 3am purge .catch((err) => logger.error(err)); } diff --git a/apps/bot/src/lib/reminderScheduler.ts b/apps/bot/src/lib/reminderScheduler.ts index 2601756..8cc692b 100644 --- a/apps/bot/src/lib/reminderScheduler.ts +++ b/apps/bot/src/lib/reminderScheduler.ts @@ -8,13 +8,18 @@ import { emojis } from '#utils/emoji.js'; import { getShardInfo, type ShardInfo, shardOwns } from '#utils/sharding.js'; import { logger } from './logger.js'; import { createShardQueue } from './queue.js'; -import { removeReminder } from './reminders.js'; +import { buildSnoozeRow, claimReminder } from './reminders.js'; interface ReminderJob { id: string; userId: string; channelId: string | null; message: string; + remindAt: number; +} + +function jobId(reminder: { id: string; remindAt: Date }) { + return `${reminder.id}-${reminder.remindAt.getTime()}`; } let queue: ReturnType> | undefined; @@ -30,7 +35,8 @@ export function initReminderScheduler(client: Client): void { async function reconcile(shard: ShardInfo): Promise { const pending = await prisma.$queryRaw` SELECT * FROM "Reminder" - WHERE ${shardOwns(Prisma.sql`COALESCE("guildId", "userId")::bigint`, shard)} + WHERE "firedAt" IS NULL + AND ${shardOwns(Prisma.sql`COALESCE("guildId", "userId")::bigint`, shard)} `; for (const reminder of pending) { @@ -47,48 +53,34 @@ export async function scheduleReminder(reminder: { }): Promise { if (!queue) return; - await unscheduleReminder(reminder.id); - - const delay = Math.max(0, reminder.remindAt.getTime() - Date.now()); + const remindAt = reminder.remindAt.getTime(); await queue.add( 'remind', - { id: reminder.id, userId: reminder.userId, channelId: reminder.channelId, message: reminder.message }, - { jobId: reminder.id, delay, removeOnComplete: true, removeOnFail: true }, + { id: reminder.id, userId: reminder.userId, channelId: reminder.channelId, message: reminder.message, remindAt }, + { jobId: jobId(reminder), delay: Math.max(0, remindAt - Date.now()), removeOnComplete: true, removeOnFail: true }, ); } -export async function unscheduleReminder(reminderId: string): Promise { - const job = await queue?.getJob(reminderId); +export async function unscheduleReminder(reminder: { id: string; remindAt: Date }): Promise { + const job = await queue?.getJob(jobId(reminder)); await job?.remove().catch(() => {}); } async function handleReminder(client: Client, reminder: ReminderJob) { - let sent = false; - if (reminder.channelId) { - const channel = client.channels.cache.get(reminder.channelId); - if (channel?.isSendable()) { - await channel.send({ - content: `${emojis.rightArrow2} <@${reminder.userId}> reminder: ${reminder.message}`, - allowedMentions: { users: [reminder.userId] }, - }); - sent = true; - } - } - if (!sent) { - await dmUser(client, reminder.userId, reminder.message); - } + if (!(await claimReminder(reminder.id, new Date(reminder.remindAt)))) return; - await removeReminder(reminder.id).catch(() => {}); -} + const payload = { + content: `${emojis.rightArrow2} <@${reminder.userId}> reminder: ${reminder.message}`, + allowedMentions: { users: [reminder.userId] }, + components: [buildSnoozeRow(reminder.id, reminder.userId)], + }; -async function dmUser(client: Client, userId: string, message: string) { - const user = await client.users.fetch(userId).catch(() => null); - if (!user) return; + const channel = reminder.channelId ? client.channels.cache.get(reminder.channelId) : undefined; + if (channel?.isSendable()) { + await channel.send(payload); + return; + } - await user - .send({ - content: `${emojis.rightArrow2} <@${userId}> reminder: ${message}`, - allowedMentions: { users: [userId] }, - }) - .catch(() => {}); + const user = await client.users.fetch(reminder.userId).catch(() => null); + await user?.send(payload).catch(() => {}); } diff --git a/apps/bot/src/lib/reminders.ts b/apps/bot/src/lib/reminders.ts index dd3e68e..f470fdc 100644 --- a/apps/bot/src/lib/reminders.ts +++ b/apps/bot/src/lib/reminders.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import { prisma } from '@questbot/database'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; import { LIMITS_ENABLED, LimitError } from './limits.js'; export async function createReminder( @@ -14,7 +15,7 @@ export async function createReminder( channelId?: string, ) { if (LIMITS_ENABLED) { - const reminderCount = await prisma.reminder.count({ where: { userId } }); + const reminderCount = await prisma.reminder.count({ where: { userId, firedAt: null } }); if (reminderCount >= 5) { throw new LimitError('You can only have up to 5 reminders.'); @@ -38,7 +39,7 @@ export async function createReminder( export async function getReminders(userId: string, guildId?: string) { return prisma.reminder.findMany({ - where: { userId, guildId }, + where: { userId, guildId, firedAt: null }, orderBy: { remindAt: 'asc' }, }); } @@ -54,3 +55,32 @@ export async function clearReminders(guildId: string, userId: string) { export async function getReminder(reminderId: string) { return prisma.reminder.findUnique({ where: { id: reminderId } }); } + +export async function claimReminder(reminderId: string, remindAt: Date) { + const { count } = await prisma.reminder.updateMany({ + where: { id: reminderId, remindAt, firedAt: null }, + data: { firedAt: new Date() }, + }); + return count > 0; +} + +export async function snoozeReminder(reminderId: string, userId: string, remindAt: Date) { + const [reminder] = await prisma.reminder.updateManyAndReturn({ + where: { id: reminderId, userId, firedAt: { gte: new Date(Date.now() - 86_400_000) } }, // 1d window for snoozing + data: { remindAt, firedAt: null }, + }); + return reminder ?? null; +} + +export async function clearExpiredReminders() { + return prisma.reminder.deleteMany({ where: { firedAt: { lt: new Date(Date.now() - 86_400_000) } } }); // ^ see above +} + +export function buildSnoozeRow(reminderId: string, userId: string) { + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`reminder-snooze:${reminderId}:${userId}`) + .setLabel('Snooze') + .setStyle(ButtonStyle.Secondary), + ); +} diff --git a/apps/bot/src/listeners/guildMemberRemove.ts b/apps/bot/src/listeners/guildMemberRemove.ts index 42384d1..155bd6f 100644 --- a/apps/bot/src/listeners/guildMemberRemove.ts +++ b/apps/bot/src/listeners/guildMemberRemove.ts @@ -24,7 +24,7 @@ export class GuildMemberRemoveListener extends Listener logger.error(err)), clearReminders(member.guild.id, member.id).catch((err) => logger.error(err)), - ...reminders.map((reminder) => unscheduleReminder(reminder.id).catch((err) => logger.error(err))), + ...reminders.map((reminder) => unscheduleReminder(reminder).catch((err) => logger.error(err))), ]); const banEntry = await getRecentAuditLogEntry(member.guild, AuditLogEvent.MemberBanAdd, member.id); diff --git a/packages/database/package.json b/packages/database/package.json index aa9b879..26461cf 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -12,10 +12,10 @@ } }, "scripts": { - "build": "prisma generate && tsc", + "build": "prisma generate --no-hints && tsc", "db:push": "prisma db push", - "db:generate": "dotenv -e ../../.env -- prisma generate", - "postinstall": "prisma generate" + "db:generate": "dotenv -e ../../.env -- prisma generate --no-hints", + "postinstall": "prisma generate --no-hints" }, "dependencies": { "@prisma/adapter-pg": "^7.10.0", diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index e8242a9..504c213 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -144,6 +144,7 @@ model Reminder { channelId String? message String remindAt DateTime + firedAt DateTime? createdAt DateTime @default(now()) server Server? @relation(fields: [guildId], references: [id], onDelete: Cascade) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4b4d59..7f5e70d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,8 +70,8 @@ importers: specifier: ^1.0.6 version: 1.0.6 undici: - specifier: ^8.11.2 - version: 8.11.2 + specifier: ^8.12.2 + version: 8.12.2 zod: specifier: ^4.6.5 version: 4.6.5 @@ -595,8 +595,8 @@ packages: cpu: [x64] os: [win32] - '@grpc/grpc-js@1.14.4': - resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + '@grpc/grpc-js@1.14.5': + resolution: {integrity: sha512-7VZM+SVdEcUUqSQeNI3zM8Qs/BhQKZndPo2h5VkYkAM8Iz0wJIa8mKV5ekQGqG8UUsnkQ0NMxIxwkIHYvj0qOw==} engines: {node: '>=12.10.0'} '@grpc/proto-loader@0.8.1': @@ -1235,6 +1235,7 @@ packages: '@opentelemetry/propagator-jaeger@2.11.0': resolution: {integrity: sha512-RUOqgoIqXOPQTOhpBrs07Y8sBlxVoLY5Fka6ueZ0prW+k6fWQsba/nHDY4gm+gmfuwtRqbX5Y9YKhDvktjbIqQ==} engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Package no longer supported. See https://github.com/open-telemetry/opentelemetry-js/pull/7077 peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -1305,12 +1306,14 @@ packages: '@opentelemetry/sdk-trace-base@2.11.0': resolution: {integrity: sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==} engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Use @opentelemetry/sdk-trace package peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' '@opentelemetry/sdk-trace-node@2.11.0': resolution: {integrity: sha512-CuvCMJmZxswhNLlM2LfuLOW3h3fZujA4hsG4B+Sz4dX2zvaXO8Ng74cnDHWD64gLszTlhiG3c0iNUjj4g+0/sA==} engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Use @opentelemetry/sdk-trace package peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -2504,8 +2507,8 @@ packages: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} - compression@1.8.1: - resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + compression@1.8.2: + resolution: {integrity: sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==} engines: {node: '>= 0.8.0'} concurrently@10.0.5: @@ -2663,6 +2666,10 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -3683,8 +3690,8 @@ packages: resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} pure-rand@6.1.0: @@ -3919,8 +3926,8 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} source-map-support@0.5.21: @@ -4062,8 +4069,8 @@ packages: resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} engines: {node: '>=18.17'} - undici@8.11.2: - resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} + undici@8.12.2: + resolution: {integrity: sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==} engines: {node: '>=22.19.0'} unified@11.0.5: @@ -4601,7 +4608,7 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@grpc/grpc-js@1.14.4': + '@grpc/grpc-js@1.14.5': dependencies: '@grpc/proto-loader': 0.8.1 '@js-sdsl/ordered-map': 4.4.2 @@ -5348,7 +5355,7 @@ snapshots: '@opentelemetry/otlp-grpc-exporter-base@0.222.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.4 + '@grpc/grpc-js': 1.14.5 '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-exporter-base': 0.222.0(@opentelemetry/api@1.9.1) @@ -5744,7 +5751,7 @@ snapshots: '@react-router/express': 8.4.0(express@5.2.1(supports-color@10.2.2))(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@7.0.2) '@react-router/node': 8.4.0(react-router@8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(typescript@7.0.2) '@remix-run/node-fetch-server': 0.14.1 - compression: 1.8.1(supports-color@10.2.2) + compression: 1.8.2(supports-color@10.2.2) express: 5.2.1(supports-color@10.2.2) morgan: 1.12.1(supports-color@10.2.2) react-router: 8.4.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -5959,7 +5966,7 @@ snapshots: jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 - source-map-js: 1.2.1 + source-map-js: 1.2.2 tailwindcss: 4.3.3 '@tailwindcss/oxide-android-arm64@4.3.3': @@ -6500,11 +6507,12 @@ snapshots: dependencies: mime-db: 1.54.0 - compression@1.8.1(supports-color@10.2.2): + compression@1.8.2(supports-color@10.2.2): dependencies: bytes: 3.1.2 compressible: 2.0.18 debug: 2.6.9(supports-color@10.2.2) + destroy: 1.2.0 negotiator: 0.6.4 on-headers: 1.1.0 safe-buffer: 5.2.1 @@ -6633,6 +6641,8 @@ snapshots: destr@2.0.5: {} + destroy@1.2.0: {} + detect-libc@2.1.2: {} devlop@1.1.0: @@ -6830,7 +6840,7 @@ snapshots: on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 - proxy-addr: 2.0.7 + proxy-addr: 2.0.8 qs: 6.15.3 range-parser: 1.3.0 router: 2.2.0(supports-color@10.2.2) @@ -7896,7 +7906,7 @@ snapshots: dependencies: nanoid: 3.3.19 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 postgres-array@2.0.0: {} @@ -7951,7 +7961,7 @@ snapshots: '@types/node': 26.6.4 long: 5.3.2 - proxy-addr@2.0.7: + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 @@ -8314,7 +8324,7 @@ snapshots: signal-exit@4.1.0: {} - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} source-map-support@0.5.21: dependencies: @@ -8461,7 +8471,7 @@ snapshots: undici@6.28.0: {} - undici@8.11.2: {} + undici@8.12.2: {} unified@11.0.5: dependencies: