diff --git a/apps/api/src/lib/helpers/openapi/message-schemas.ts b/apps/api/src/lib/helpers/openapi/message-schemas.ts index 85af24c..3be160f 100644 --- a/apps/api/src/lib/helpers/openapi/message-schemas.ts +++ b/apps/api/src/lib/helpers/openapi/message-schemas.ts @@ -16,10 +16,22 @@ export const messageReactionSchema = z.object({ reactedByCurrentUser: z.boolean(), }) +const httpsUrlSchema = z.string().regex(/^https?:\/\//i) + +export const messageEmbedSchema = z.object({ + type: z.enum(["link", "image", "video", "rich"]), + url: httpsUrlSchema, + title: z.string().optional(), + description: z.string().optional(), + thumbnail: httpsUrlSchema.optional(), + siteName: z.string().optional(), +}) + export const messageWithAuthorSchema = selectMessageSchema.extend({ author: messageAuthorSchema, mentions: z.array(messageAuthorSchema), reactions: z.array(messageReactionSchema), + embeds: z.array(messageEmbedSchema), }) export const listMessagesQuerySchema = paginationQuerySchema diff --git a/apps/api/src/lib/queries/messages.ts b/apps/api/src/lib/queries/messages.ts index 3cae024..7a98996 100644 --- a/apps/api/src/lib/queries/messages.ts +++ b/apps/api/src/lib/queries/messages.ts @@ -135,6 +135,7 @@ export async function fetchMessagePage( const messagesWithMentions = messages.map((msg) => ({ ...msg, + embeds: msg.embeds ?? [], mentions: mentionsByMessageId.get(msg.id) ?? [], reactions: Array.from(reactionsByMessageId.get(msg.id)?.values() ?? []), })) diff --git a/apps/realtime/package.json b/apps/realtime/package.json index 732d59e..16eb6fb 100644 --- a/apps/realtime/package.json +++ b/apps/realtime/package.json @@ -10,11 +10,12 @@ "check-types": "tsc --noEmit" }, "dependencies": { - "@socket.io/redis-adapter": "^8.3.0", "@repo/auth": "workspace:*", "@repo/db": "workspace:*", "@repo/env": "workspace:*", "@repo/realtime-types": "workspace:*", + "@socket.io/redis-adapter": "^8.3.0", + "bullmq": "^5.52.2", "redis": "^4.7.0", "socket.io": "^4.8.1", "zod": "^4.3.6" diff --git a/apps/realtime/src/index.ts b/apps/realtime/src/index.ts index d9055eb..ff1607f 100644 --- a/apps/realtime/src/index.ts +++ b/apps/realtime/src/index.ts @@ -17,7 +17,10 @@ import { toggleMessageReactionPayloadSchema, userRoom, } from "@repo/realtime-types" +import type { LinkUnfurlJobData } from "@repo/realtime-types/queues" +import { LINK_UNFURL_QUEUE } from "@repo/realtime-types/queues" import { createAdapter } from "@socket.io/redis-adapter" +import { Queue } from "bullmq" import { createClient } from "redis" import { Server, type Socket } from "socket.io" import { toErrorMessage } from "@/lib/errors" @@ -327,6 +330,23 @@ io.on("connection", (socket) => { } ack?.({ ok: true, message: messageWithMentions }) + + // Enqueue link unfurl job if the message contains a URL + const rawUrlMatches = parsed.content.match(/https?:\/\/[^\s<>"]+/g) + const urlMatches = rawUrlMatches?.map((u) => + u.replace(/[.,!?:;'")\]]+$/, "") + ) + if (urlMatches && urlMatches.length > 0) { + void linkUnfurlQueue + .add("unfurl", { + messageId: createdMessage.message.id, + channelId: parsed.channelId, + urls: urlMatches, + }) + .catch((err) => { + console.error("[realtime] failed to enqueue link-unfurl:", err) + }) + } } catch (error) { ack?.({ ok: false, error: toErrorMessage(error) }) } @@ -400,6 +420,30 @@ io.on("connection", (socket) => { }) }) +function parseRedisUrl(url: string) { + const parsed = new URL(url) + const dbIndex = Number.parseInt(parsed.pathname.slice(1), 10) + return { + host: parsed.hostname, + port: Number(parsed.port) || 6379, + password: parsed.password || undefined, + username: parsed.username || undefined, + tls: parsed.protocol === "rediss:" ? {} : undefined, + db: Number.isFinite(dbIndex) ? dbIndex : 0, + } +} + +const linkUnfurlQueue = new Queue(LINK_UNFURL_QUEUE, { + connection: { + ...parseRedisUrl(env.REDIS_URL), + maxRetriesPerRequest: null, + }, + defaultJobOptions: { + removeOnComplete: { age: 3600, count: 1000 }, + removeOnFail: { age: 86400, count: 5000 }, + }, +}) + async function bootstrap() { await Promise.all([ redisPubClient.connect(), diff --git a/apps/realtime/src/services/messages.ts b/apps/realtime/src/services/messages.ts index 092893f..4967689 100644 --- a/apps/realtime/src/services/messages.ts +++ b/apps/realtime/src/services/messages.ts @@ -100,6 +100,7 @@ export async function createMessage(input: CreateMessageInput) { }, mentions: [], reactions: [], + embeds: [], } if (input.payload.nonce) { diff --git a/apps/web/package.json b/apps/web/package.json index e56bff9..0336b61 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,6 +22,7 @@ "@tailwindcss/postcss": "^4.1.18", "@tanstack/react-query": "^5.90.21", "@tanstack/react-router": "^1.120.3", + "@tiptap/extension-link": "^3.20.0", "@tiptap/extension-mention": "^3.20.0", "@tiptap/markdown": "^3.20.0", "@tiptap/pm": "^3.20.0", diff --git a/apps/web/src/components/chat/composer/message-input.tsx b/apps/web/src/components/chat/composer/message-input.tsx index 5cdce51..93ae341 100644 --- a/apps/web/src/components/chat/composer/message-input.tsx +++ b/apps/web/src/components/chat/composer/message-input.tsx @@ -5,6 +5,7 @@ import { PopoverTrigger, } from "@repo/ui/components/popover" import { cn } from "@repo/ui/lib/utils" +import Link from "@tiptap/extension-link" import Mention, { type MentionOptions } from "@tiptap/extension-mention" import { Markdown } from "@tiptap/markdown" import { PluginKey } from "@tiptap/pm/state" @@ -102,15 +103,21 @@ interface MessageInputProps { } function toStoredMarkdown(markdown: string) { - return markdown - .replace(/\u00A0/g, " ") - .replace(TIPTAP_MARKDOWN_MENTION_REGEX, (_match, mentionId: string) => { - if (mentionId.toLowerCase() === EVERYONE_MENTION_ID) { - return "@everyone" - } + return ( + markdown + .replace(/\u00A0/g, " ") + // Strip ++…++ wrappers the Markdown extension generates for unrecognised marks (e.g. Link) + // TipTap outputs either ++[url](url)++ or ++bareUrl++ + .replace(/\+\+\[([^\]]+)\]\([^)]+\)\+\+/g, "$1") + .replace(/\+\+([\s\S]+?)\+\+/g, "$1") + .replace(TIPTAP_MARKDOWN_MENTION_REGEX, (_match, mentionId: string) => { + if (mentionId.toLowerCase() === EVERYONE_MENTION_ID) { + return "@everyone" + } - return `<@${mentionId}>` - }) + return `<@${mentionId}>` + }) + ) } function extractMentionIds(content: string) { @@ -480,6 +487,17 @@ export function MessageInput({ horizontalRule: false, }), Markdown, + Link.configure({ + openOnClick: false, + autolink: true, + linkOnPaste: true, + HTMLAttributes: { + class: + "text-primary underline-offset-2 hover:underline cursor-pointer", + rel: "noreferrer noopener", + target: "_blank", + }, + }), Mention.configure({ HTMLAttributes: { class: "rounded bg-primary/15 px-1 py-0.5 font-medium text-primary", @@ -509,7 +527,8 @@ export function MessageInput({ const handleSend = useCallback(() => { if (!editor) return - const markdown = toStoredMarkdown(editor.getMarkdown()) + const rawMarkdown = editor.getMarkdown() + const markdown = toStoredMarkdown(rawMarkdown) const trimmed = markdown.trim() if (!trimmed || trimmed.length > MAX_MESSAGE_LENGTH || isSending) return diff --git a/apps/web/src/components/chat/embed-card.tsx b/apps/web/src/components/chat/embed-card.tsx new file mode 100644 index 0000000..725913d --- /dev/null +++ b/apps/web/src/components/chat/embed-card.tsx @@ -0,0 +1,74 @@ +import { cn } from "@repo/ui/lib/utils" +import { ExternalLink } from "lucide-react" +import type { Message } from "@/lib/api-types" + +type Embed = Message["embeds"][number] + +interface EmbedCardProps { + embed: Embed + className?: string +} + +export function EmbedCard({ embed, className }: EmbedCardProps) { + const hasMeta = Boolean(embed.title || embed.description) + + return ( +
+
+ {embed.title && ( + + {embed.title} + + )} + {embed.description && ( +

+ {embed.description} +

+ )} + {!hasMeta && ( + + + {embed.url} + + )} +
+ {embed.thumbnail && + (embed.title || embed.description || embed.siteName) && ( +
+ {embed.title { + e.currentTarget.style.display = "none" + }} + /> +
+ )} + {embed.siteName && ( +
+ + {embed.siteName} + +
+ )} +
+ ) +} diff --git a/apps/web/src/components/chat/message-item.tsx b/apps/web/src/components/chat/message-item.tsx index ed03bf5..e37bd16 100644 --- a/apps/web/src/components/chat/message-item.tsx +++ b/apps/web/src/components/chat/message-item.tsx @@ -3,6 +3,7 @@ import { cn } from "@repo/ui/lib/utils" import { formatTime } from "@repo/utils/date" import { useCallback, useState } from "react" import type { Message } from "@/lib/api-types" +import { EmbedCard } from "./embed-card" import { MessageActionBar } from "./message-action-bar" import { MessageMarkdown } from "./message-markdown" @@ -89,6 +90,13 @@ export function MessageItem({ content={message.content} mentions={message.mentions} /> + {message.embeds.length > 0 && ( +
+ {message.embeds.map((embed, index) => ( + + ))} +
+ )} {message.reactions.length > 0 && (
{message.reactions.map((reaction) => ( diff --git a/apps/web/src/hooks/use-message-sending.ts b/apps/web/src/hooks/use-message-sending.ts index 773e76a..065191a 100644 --- a/apps/web/src/hooks/use-message-sending.ts +++ b/apps/web/src/hooks/use-message-sending.ts @@ -1,3 +1,4 @@ +import type { RealtimeMessageEmbedsUpdated } from "@repo/realtime-types" import type { QueryClient } from "@tanstack/react-query" import { useCallback, useEffect, useRef } from "react" import type { Message } from "@/lib/api-types" @@ -83,6 +84,25 @@ export function useMessageSending({ } }, [socket, channelId, updateMessagesInCache]) + useEffect(() => { + if (!socket) return + + const handleEmbedsUpdated = (update: RealtimeMessageEmbedsUpdated) => { + if (update.channelId !== channelId) return + + updateMessagesInCache((messages) => + messages.map((m) => + m.id === update.messageId ? { ...m, embeds: update.embeds } : m + ) + ) + } + + socket.on("message:embeds:updated", handleEmbedsUpdated) + return () => { + socket.off("message:embeds:updated", handleEmbedsUpdated) + } + }, [socket, channelId, updateMessagesInCache]) + const handleSend = useCallback( (content: string, options?: { mentions: Message["mentions"] }) => { if (!socket?.connected || !currentUser) return diff --git a/apps/web/src/lib/realtime-adapter.ts b/apps/web/src/lib/realtime-adapter.ts index a7a9702..1ce297d 100644 --- a/apps/web/src/lib/realtime-adapter.ts +++ b/apps/web/src/lib/realtime-adapter.ts @@ -15,7 +15,7 @@ export function realtimeMessageToMessage(rm: RealtimeMessage): Message { author: rm.author, referencedMessageId: null, attachments: [], - embeds: [], + embeds: rm.embeds ?? [], pinned: false, editedAt: null, mentions: rm.mentions, diff --git a/apps/worker/package.json b/apps/worker/package.json new file mode 100644 index 0000000..836f89d --- /dev/null +++ b/apps/worker/package.json @@ -0,0 +1,26 @@ +{ + "name": "@repo/worker", + "version": "0.1.0", + "type": "module", + "private": true, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsup", + "start": "node dist/index.js", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@repo/db": "workspace:*", + "@repo/env": "workspace:*", + "@repo/realtime-types": "workspace:*", + "@socket.io/redis-emitter": "^5.1.0", + "bullmq": "^5.70.2", + "open-graph-scraper": "^6.11.0", + "redis": "^4.7.1" + }, + "devDependencies": { + "@repo/typescript-config": "workspace:*", + "tsup": "^8.5.1", + "tsx": "^4.21.0" + } +} diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts new file mode 100644 index 0000000..d54adbb --- /dev/null +++ b/apps/worker/src/index.ts @@ -0,0 +1,76 @@ +import { env } from "@repo/env/server" +import type { ServerToClientEvents } from "@repo/realtime-types/events" +import { LINK_UNFURL_QUEUE } from "@repo/realtime-types/queues" +import { Emitter } from "@socket.io/redis-emitter" +import { Worker } from "bullmq" +import { createClient } from "redis" +import { createLinkUnfurlProcessor } from "@/jobs/link-unfurl" + +function parseRedisUrl(url: string) { + const parsed = new URL(url) + const dbIndex = Number.parseInt(parsed.pathname.slice(1), 10) + return { + host: parsed.hostname, + port: Number(parsed.port) || 6379, + password: parsed.password || undefined, + username: parsed.username || undefined, + tls: parsed.protocol === "rediss:" ? {} : undefined, + db: Number.isFinite(dbIndex) ? dbIndex : 0, + } +} + +async function bootstrap() { + // Redis client for Socket.IO emitter (uses `redis` v4 package) + const redisEmitterClient = createClient({ url: env.REDIS_URL }) + redisEmitterClient.on("error", (error) => { + console.error("[worker] redis emitter error:", error) + }) + await redisEmitterClient.connect() + + const emitter = new Emitter(redisEmitterClient) + + // BullMQ uses ioredis internally — pass connection options, not an instance + const redisOpts = parseRedisUrl(env.REDIS_URL) + + const linkUnfurlWorker = new Worker( + LINK_UNFURL_QUEUE, + createLinkUnfurlProcessor(emitter), + { + connection: { + ...redisOpts, + maxRetriesPerRequest: null, + }, + concurrency: 5, + } + ) + + linkUnfurlWorker.on("failed", (job, error) => { + console.error(`[worker] link-unfurl job ${job?.id} failed:`, error.message) + }) + + linkUnfurlWorker.on("error", (error) => { + console.error("[worker] link-unfurl worker error:", { + queue: LINK_UNFURL_QUEUE, + workerId: linkUnfurlWorker.id, + message: error.message, + stack: error.stack, + }) + }) + + console.log("Worker started, processing queues:", LINK_UNFURL_QUEUE) + + const shutdown = async () => { + console.log("[worker] shutting down...") + await linkUnfurlWorker.close() + await redisEmitterClient.quit() + process.exit(0) + } + + process.on("SIGINT", shutdown) + process.on("SIGTERM", shutdown) +} + +bootstrap().catch((error) => { + console.error("[worker] failed to start:", error) + process.exit(1) +}) diff --git a/apps/worker/src/jobs/link-unfurl.ts b/apps/worker/src/jobs/link-unfurl.ts new file mode 100644 index 0000000..2d0dc8f --- /dev/null +++ b/apps/worker/src/jobs/link-unfurl.ts @@ -0,0 +1,137 @@ +import { lookup } from "node:dns/promises" +import { db, eq, schema } from "@repo/db" +import type { Embed } from "@repo/db/schema" +import type { + LinkUnfurlJobData, + RealtimeMessageEmbedsUpdated, +} from "@repo/realtime-types" +import type { ServerToClientEvents } from "@repo/realtime-types/events" +import { channelRoom } from "@repo/realtime-types/rooms" +import type { Emitter } from "@socket.io/redis-emitter" +import type { Job } from "bullmq" +import ogs from "open-graph-scraper" + +const OG_FETCH_TIMEOUT_MS = 5000 + +const PRIVATE_IP_REGEX = + /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|0\.|169\.254\.|::1|fc|fd|fe80)/i + +async function isSafeUrl(urlString: string): Promise { + try { + const parsed = new URL(urlString) + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") + return false + + const hostname = parsed.hostname + if ( + hostname === "localhost" || + hostname === "[::1]" || + PRIVATE_IP_REGEX.test(hostname) + ) + return false + + const addresses = await lookup(hostname, { all: true }) + for (const { address } of addresses) { + if (PRIVATE_IP_REGEX.test(address)) return false + } + + return true + } catch { + return false + } +} + +const OG_PROXY_RULES: Array<{ + pattern: RegExp + proxyHost: string + siteName: string +}> = [ + { + pattern: /^https?:\/\/(www\.)?(x\.com|twitter\.com)\//, + proxyHost: "fxtwitter.com", + siteName: "X (formerly Twitter)", + }, +] + +function matchProxyRule(originalUrl: string) { + for (const rule of OG_PROXY_RULES) { + if (rule.pattern.test(originalUrl)) { + try { + const parsed = new URL(originalUrl) + parsed.hostname = rule.proxyHost + return { fetchUrl: parsed.toString(), siteName: rule.siteName } + } catch { + return null + } + } + } + return null +} + +async function fetchOgEmbed(url: string): Promise { + const proxy = matchProxyRule(url) + const fetchUrl = proxy?.fetchUrl ?? url + + if (!(await isSafeUrl(fetchUrl))) return null + + try { + const { error, result } = await ogs({ + url: fetchUrl, + timeout: OG_FETCH_TIMEOUT_MS, + fetchOptions: { + headers: { + "user-agent": "Townhall/1.0 OGBot", + }, + redirect: "error", + }, + }) + + if (error || !result.success) { + return null + } + + const imageUrl = + result.ogImage?.[0]?.url ?? result.twitterImage?.[0]?.url ?? undefined + + return { + type: "link", + url, + title: result.ogTitle ?? result.twitterTitle ?? undefined, + description: + result.ogDescription ?? result.twitterDescription ?? undefined, + thumbnail: imageUrl, + siteName: proxy?.siteName ?? result.ogSiteName ?? undefined, + } + } catch { + return null + } +} + +export function createLinkUnfurlProcessor( + emitter: Emitter +) { + return async (job: Job) => { + const { messageId, channelId, urls } = job.data + if (urls.length === 0) return + + const results = await Promise.all(urls.map(fetchOgEmbed)) + const embeds = results.filter((e): e is Embed => e !== null) + if (embeds.length === 0) return + + const [updated] = await db + .update(schema.message) + .set({ embeds }) + .where(eq(schema.message.id, messageId)) + .returning({ id: schema.message.id }) + + if (!updated) return + + const payload: RealtimeMessageEmbedsUpdated = { + channelId, + messageId, + embeds, + } + + emitter.to(channelRoom(channelId)).emit("message:embeds:updated", payload) + } +} diff --git a/apps/worker/tsconfig.json b/apps/worker/tsconfig.json new file mode 100644 index 0000000..24ae862 --- /dev/null +++ b/apps/worker/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "baseUrl": ".", + "outDir": "dist", + "rootDir": "src", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/worker/tsup.config.ts b/apps/worker/tsup.config.ts new file mode 100644 index 0000000..cf9e854 --- /dev/null +++ b/apps/worker/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "tsup" + +export default defineConfig({ + entry: ["src/index.ts"], + format: "esm", + outDir: "dist", + noExternal: [/@repo\/.*/], +}) diff --git a/packages/db/src/schemas/messages.ts b/packages/db/src/schemas/messages.ts index 39c4a65..90bf004 100644 --- a/packages/db/src/schemas/messages.ts +++ b/packages/db/src/schemas/messages.ts @@ -105,4 +105,5 @@ export type Embed = { title?: string description?: string thumbnail?: string + siteName?: string } diff --git a/packages/realtime-types/package.json b/packages/realtime-types/package.json index c5828d4..1e4879d 100644 --- a/packages/realtime-types/package.json +++ b/packages/realtime-types/package.json @@ -6,6 +6,7 @@ "exports": { ".": "./src/index.ts", "./events": "./src/events.ts", + "./queues": "./src/queues.ts", "./rooms": "./src/rooms.ts" }, "scripts": { diff --git a/packages/realtime-types/src/events.ts b/packages/realtime-types/src/events.ts index c8b60a0..8e0c1cf 100644 --- a/packages/realtime-types/src/events.ts +++ b/packages/realtime-types/src/events.ts @@ -67,6 +67,15 @@ export type RealtimeMessageReaction = { reactedByCurrentUser: boolean } +export type RealtimeEmbed = { + type: "link" | "image" | "video" | "rich" + url: string + title?: string + description?: string + thumbnail?: string + siteName?: string +} + export type RealtimeMessage = { id: string channelId: string @@ -84,9 +93,16 @@ export type RealtimeMessage = { } mentions: RealtimeMessageMention[] reactions: RealtimeMessageReaction[] + embeds: RealtimeEmbed[] nonce?: string } +export type RealtimeMessageEmbedsUpdated = { + channelId: string + messageId: string + embeds: RealtimeEmbed[] +} + export type RealtimeMessageReactionUpdated = { channelId: string messageId: string @@ -176,6 +192,7 @@ export interface ServerToClientEvents { "presence:user:update": (payload: PresenceUserUpdate) => void "message:created": (payload: RealtimeMessage) => void "message:reaction:updated": (payload: RealtimeMessageReactionUpdated) => void + "message:embeds:updated": (payload: RealtimeMessageEmbedsUpdated) => void "notification:unread": (payload: UnreadNotification) => void "notification:mention": (payload: MentionNotification) => void "channel:read-state": (payload: ChannelReadState) => void diff --git a/packages/realtime-types/src/index.ts b/packages/realtime-types/src/index.ts index dffcb93..14cbbfa 100644 --- a/packages/realtime-types/src/index.ts +++ b/packages/realtime-types/src/index.ts @@ -1,2 +1,3 @@ export * from "./events" +export * from "./queues" export * from "./rooms" diff --git a/packages/realtime-types/src/queues.ts b/packages/realtime-types/src/queues.ts new file mode 100644 index 0000000..f108051 --- /dev/null +++ b/packages/realtime-types/src/queues.ts @@ -0,0 +1,7 @@ +export const LINK_UNFURL_QUEUE = "link-unfurl" + +export type LinkUnfurlJobData = { + messageId: string + channelId: string + urls: string[] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a65138..139b44e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.6) + bullmq: + specifier: ^5.52.2 + version: 5.70.2 redis: specifier: ^4.7.0 version: 4.7.1 @@ -154,6 +157,9 @@ importers: '@tanstack/react-router': specifier: ^1.120.3 version: 1.159.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/extension-link': + specifier: ^3.20.0 + version: 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0) '@tiptap/extension-mention': specifier: ^3.20.0 version: 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)(@tiptap/suggestion@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)) @@ -255,6 +261,40 @@ importers: specifier: ^6.3.5 version: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + apps/worker: + dependencies: + '@repo/db': + specifier: workspace:* + version: link:../../packages/db + '@repo/env': + specifier: workspace:* + version: link:../../packages/env + '@repo/realtime-types': + specifier: workspace:* + version: link:../../packages/realtime-types + '@socket.io/redis-emitter': + specifier: ^5.1.0 + version: 5.1.0 + bullmq: + specifier: ^5.70.2 + version: 5.70.2 + open-graph-scraper: + specifier: ^6.11.0 + version: 6.11.0 + redis: + specifier: ^4.7.1 + version: 4.7.1 + devDependencies: + '@repo/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + tsx: + specifier: ^4.21.0 + version: 4.21.0 + apps/www: dependencies: '@hookform/resolvers': @@ -1440,6 +1480,9 @@ packages: '@types/node': optional: true + '@ioredis/commands@1.5.0': + resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -1474,6 +1517,36 @@ packages: '@cfworker/json-schema': optional: true + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + '@mswjs/interceptors@0.41.2': resolution: {integrity: sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw==} engines: {node: '>=18'} @@ -2439,6 +2512,9 @@ packages: peerDependencies: socket.io-adapter: ^2.5.4 + '@socket.io/redis-emitter@5.1.0': + resolution: {integrity: sha512-QQUFPBq6JX7JIuM/X1811ymKlAfwufnQ8w6G2/59Jaqp09hdF1GJ/+e8eo/XdcmT0TqkvcSa2TT98ggTXa5QYw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -3045,6 +3121,9 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -3057,6 +3136,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bullmq@5.70.2: + resolution: {integrity: sha512-k8JqTUiKKiO7QMyIiVh0NOstmsYa06KnIY+ZplakHcu8I8DZqwwI13u5NK2obi8DFjBrzBuDeO2JXbBEdTP8lg==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -3109,6 +3191,16 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -3230,10 +3322,21 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3303,6 +3406,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -3325,6 +3432,19 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@17.2.4: resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} engines: {node: '>=12'} @@ -3461,6 +3581,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -3483,6 +3606,14 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -3783,6 +3914,9 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3804,6 +3938,10 @@ packages: engines: {node: '>=18'} hasBin: true + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -3822,6 +3960,10 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + ioredis@5.9.3: + resolution: {integrity: sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==} + engines: {node: '>=12.22.0'} + ip-address@10.0.1: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} @@ -4071,6 +4213,12 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -4320,6 +4468,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + msw@2.12.9: resolution: {integrity: sha512-NYbi51C6M3dujGmcmuGemu68jy12KqQPoVWGeroKToLGsBgrwG5ErM8WctoIIg49/EV49SEvYM9WSqO4G7kNeQ==} engines: {node: '>=18'} @@ -4381,6 +4536,9 @@ packages: sass: optional: true + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -4390,6 +4548,10 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -4408,6 +4570,9 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4439,6 +4604,10 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + open-graph-scraper@6.11.0: + resolution: {integrity: sha512-KkO3qMMzJj9KYGtCl19dRtncb+RuBiG/P9BgukcAG4p2w9wSAWTE90vL6/xqth1K9ThkYF/+xfTGrVvU79TJtQ==} + engines: {node: '>=20.0.0'} + open@11.0.0: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} @@ -4474,6 +4643,15 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4777,6 +4955,14 @@ packages: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + redis@4.7.1: resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} @@ -4978,6 +5164,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -5225,6 +5414,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -5299,6 +5492,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + validate-npm-package-name@7.0.2: resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} engines: {node: ^20.17.0 || >=22.9.0} @@ -5363,6 +5560,15 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -6189,6 +6395,8 @@ snapshots: optionalDependencies: '@types/node': 25.2.2 + '@ioredis/commands@1.5.0': {} + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.1': @@ -6236,6 +6444,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + '@mswjs/interceptors@0.41.2': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -7173,6 +7399,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@socket.io/redis-emitter@5.1.0': + dependencies: + debug: 4.3.7 + notepack.io: 3.0.1 + socket.io-parser: 4.2.5 + transitivePeerDependencies: + - supports-color + '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -7776,6 +8010,8 @@ snapshots: transitivePeerDependencies: - supports-color + boolbase@1.0.0: {} + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -7790,6 +8026,18 @@ snapshots: buffer-from@1.1.2: {} + bullmq@5.70.2: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.9.3 + msgpackr: 1.11.5 + node-abort-controller: 3.1.1 + semver: 7.7.4 + tslib: 2.8.1 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -7829,6 +8077,31 @@ snapshots: character-reference-invalid@2.0.1: {} + chardet@2.1.1: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.22.0 + whatwg-mimetype: 4.0.0 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -7933,12 +8206,26 @@ snapshots: crelt@1.0.6: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -7978,6 +8265,8 @@ snapshots: defu@6.1.4: {} + denque@2.1.0: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -7992,6 +8281,24 @@ snapshots: diff@8.0.3: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv@17.2.4: {} drizzle-kit@0.31.9: @@ -8041,6 +8348,11 @@ snapshots: encodeurl@2.0.0: {} + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -8082,6 +8394,10 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + + entities@7.0.1: {} + env-paths@2.2.1: {} error-ex@1.3.4: @@ -8475,6 +8791,13 @@ snapshots: html-url-attributes@3.0.1: {} + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -8496,6 +8819,10 @@ snapshots: husky@9.1.7: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -8511,6 +8838,20 @@ snapshots: inline-style-parser@0.2.7: {} + ioredis@5.9.3: + dependencies: + '@ioredis/commands': 1.5.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@10.0.1: {} ipaddr.js@1.9.1: {} @@ -8675,6 +9016,10 @@ snapshots: load-tsconfig@0.2.5: {} + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + log-symbols@6.0.0: dependencies: chalk: 5.6.2 @@ -9121,6 +9466,22 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 + msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3): dependencies: '@inquirer/confirm': 5.1.21(@types/node@25.2.2) @@ -9191,6 +9552,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-abort-controller@3.1.1: {} + node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -9199,6 +9562,11 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + node-releases@2.0.27: {} normalize-path@3.0.0: {} @@ -9214,6 +9582,10 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -9238,6 +9610,13 @@ snapshots: dependencies: mimic-function: 5.0.1 + open-graph-scraper@6.11.0: + dependencies: + chardet: 2.1.1 + cheerio: 1.2.0 + iconv-lite: 0.7.2 + undici: 7.22.0 + open@11.0.0: dependencies: default-browser: 5.5.0 @@ -9292,6 +9671,19 @@ snapshots: parse-ms@4.0.0: {} + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -9689,6 +10081,12 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + redis@4.7.1: dependencies: '@redis/bloom': 1.2.0(@redis/client@1.6.1) @@ -9822,8 +10220,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: - optional: true + semver@7.7.4: {} send@1.2.1: dependencies: @@ -10036,6 +10433,8 @@ snapshots: split2@4.2.0: {} + standard-as-callback@2.1.0: {} + statuses@2.0.2: {} stdin-discarder@0.2.2: {} @@ -10262,6 +10661,8 @@ snapshots: undici-types@7.16.0: {} + undici@7.22.0: {} + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -10342,6 +10743,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + validate-npm-package-name@7.0.2: {} vary@1.1.2: {} @@ -10378,6 +10781,12 @@ snapshots: webpack-virtual-modules@0.6.2: {} + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + which@2.0.2: dependencies: isexe: 2.0.0