From 59ac3c7d1405583ff79bcfd5fc616ff283a81099 Mon Sep 17 00:00:00 2001 From: wuzoo Date: Fri, 3 Apr 2026 20:01:03 +0900 Subject: [PATCH 01/11] =?UTF-8?q?chore:=20slack=20package=20export=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/slack/package.json | 4 +- packages/slack/src/index.ts | 57 ------------------- packages/slack/src/thread.ts | 55 ++++++++++++++++++ .../src/handler/figma-plugin/design-review.ts | 2 +- .../handler/github-webhook/pull_request.ts | 4 +- servers/mumu/src/slack/index.ts | 3 +- 6 files changed, 64 insertions(+), 61 deletions(-) create mode 100644 packages/slack/src/thread.ts diff --git a/packages/slack/package.json b/packages/slack/package.json index 31ff920..3648dfe 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -7,7 +7,9 @@ "typecheck": "tsc --noEmit" }, "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./client": "./src/client.ts", + "./thread": "./src/thread.ts" }, "dependencies": { "@makers-devops/redis": "workspace:*", diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts index 7a2eec6..433bb61 100644 --- a/packages/slack/src/index.ts +++ b/packages/slack/src/index.ts @@ -1,59 +1,2 @@ -import type { ChatPostMessageArguments } from "@slack/web-api"; -import type { SlackThreadData, SlackThreadMessage } from "./types"; -import { slackClient } from "./client"; -import { getSlackThreadData, setSlackThreadData } from "./redis"; - -export * from "./client"; export * from "./types"; export * from "./redis"; - -function toChatPostMessageArgs({ channel, message }: SlackThreadMessage): ChatPostMessageArguments { - if (message.blocks !== undefined) { - return { - channel, - text: message.text, - blocks: message.blocks, - }; - } - - return { - channel, - text: message.text, - }; -} - -export const createSlackThread = async (id: string, _message: SlackThreadMessage) => { - try { - const response = await slackClient.chat.postMessage(toChatPostMessageArgs(_message)); - - if (response.ok) { - const data: SlackThreadData = { - id, - version: 1, - channel: response.channel ?? _message.channel, - thread_ts: response.ts ?? "", - }; - await setSlackThreadData(id, data, _message.ex ? { ex: _message.ex } : undefined); - - return data; - } - - return null; - } catch (error) { - console.error(error); - return null; - } -}; - -export const findSlackThread = async (id: string) => { - try { - const data = await getSlackThreadData(id); - - if (data) { - return data; - } - } catch (error) { - console.error(error); - } - return null; -}; diff --git a/packages/slack/src/thread.ts b/packages/slack/src/thread.ts new file mode 100644 index 0000000..0c35b00 --- /dev/null +++ b/packages/slack/src/thread.ts @@ -0,0 +1,55 @@ +import type { SlackThreadMessage } from "./types"; +import { slackClient } from "./client"; +import { setSlackThreadData, getSlackThreadData } from "./redis"; +import type { SlackThreadData } from "./types"; + +function toChatPostMessageArgs({ channel, message }: SlackThreadMessage) { + if (message.blocks !== undefined) { + return { + channel, + text: message.text, + blocks: message.blocks, + }; + } + + return { + channel, + text: message.text, + }; +} + +export const createSlackThread = async (id: string, _message: SlackThreadMessage) => { + try { + const response = await slackClient.chat.postMessage(toChatPostMessageArgs(_message)); + + if (response.ok) { + const data: SlackThreadData = { + id, + version: 1, + channel: response.channel ?? _message.channel, + thread_ts: response.ts ?? "", + }; + await setSlackThreadData(id, data, _message.ex ? { ex: _message.ex } : undefined); + + return data; + } + + return null; + } catch (error) { + console.error(error); + return null; + } +}; + +export const findSlackThread = async (id: string) => { + try { + const data = await getSlackThreadData(id); + + if (data) { + return data; + } + } catch (error) { + console.error(error); + } + return null; +}; diff --git a/servers/mumu/src/handler/figma-plugin/design-review.ts b/servers/mumu/src/handler/figma-plugin/design-review.ts index 8eba05e..3d5c6a2 100644 --- a/servers/mumu/src/handler/figma-plugin/design-review.ts +++ b/servers/mumu/src/handler/figma-plugin/design-review.ts @@ -1,5 +1,5 @@ import { designReviewRequestBodySchema } from "@makers-devops/figma"; -import { slackClient } from "@makers-devops/slack"; +import { slackClient } from "@makers-devops/slack/client"; import type { Request, Response } from "express"; import { CHANNELS } from "../../constant"; import { 피그마_리뷰_요청 } from "@makers-devops/slack-blocks"; diff --git a/servers/mumu/src/handler/github-webhook/pull_request.ts b/servers/mumu/src/handler/github-webhook/pull_request.ts index 704abdb..4e61233 100644 --- a/servers/mumu/src/handler/github-webhook/pull_request.ts +++ b/servers/mumu/src/handler/github-webhook/pull_request.ts @@ -2,7 +2,9 @@ import type { Request, Response } from "express"; import { pullRequestSchema, type PullRequest } from "@makers-devops/github"; import { createPullRequestThread } from "../../slack"; import { getPullRequestThreadKey } from "../../slack/key"; -import { deleteSlackThreadData, findSlackThread, slackClient } from "@makers-devops/slack"; +import { slackClient } from "@makers-devops/slack/client"; +import { findSlackThread } from "@makers-devops/slack/thread"; +import { deleteSlackThreadData } from "@makers-devops/slack"; import { PR_닫힘 } from "@makers-devops/slack-blocks"; import { assignReviewersAndAssignee } from "../../github"; import { selectReviewers } from "../../github/review"; diff --git a/servers/mumu/src/slack/index.ts b/servers/mumu/src/slack/index.ts index f45b0a4..bc99a7f 100644 --- a/servers/mumu/src/slack/index.ts +++ b/servers/mumu/src/slack/index.ts @@ -1,4 +1,5 @@ -import { createSlackThread, findSlackThread, slackClient } from "@makers-devops/slack"; +import { slackClient } from "@makers-devops/slack/client"; +import { createSlackThread, findSlackThread } from "@makers-devops/slack/thread"; import { getPullRequestThreadKey } from "./key"; import type { PullRequest, PullRequestReviewComment } from "@makers-devops/github"; import { CHANNELS } from "../constant"; From f1968138ff97847192832509ef663fc27ffa3a4c Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sat, 4 Apr 2026 17:22:48 +0900 Subject: [PATCH 02/11] chore: env setting --- servers/geek-news-bot/.env.example | 5 +++++ servers/geek-news-bot/src/env.ts | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 servers/geek-news-bot/.env.example create mode 100644 servers/geek-news-bot/src/env.ts diff --git a/servers/geek-news-bot/.env.example b/servers/geek-news-bot/.env.example new file mode 100644 index 0000000..fa4cbcf --- /dev/null +++ b/servers/geek-news-bot/.env.example @@ -0,0 +1,5 @@ +SLACK_BOT_TOKEN= +SLACK_CHANNEL_ID= +GOOGLE_GENERATIVE_AI_API_KEY= +CRON_SCHEDULE= +MAX_NEWS_PER_FETCH= \ No newline at end of file diff --git a/servers/geek-news-bot/src/env.ts b/servers/geek-news-bot/src/env.ts new file mode 100644 index 0000000..ae2e794 --- /dev/null +++ b/servers/geek-news-bot/src/env.ts @@ -0,0 +1,20 @@ +import "dotenv/config"; +import { assert } from "@makers-devops/shared"; + +const assertEnv = (env: string) => { + assert(process.env[env] != null, `${env} is not set`); + + return process.env[env] as string; +}; + +const optionalEnv = (env: string) => { + return process.env[env] ?? undefined; +}; + +export const ENV = { + slackBotToken: assertEnv("SLACK_BOT_TOKEN"), + slackChannelId: assertEnv("SLACK_CHANNEL_ID"), + googleApiKey: assertEnv("GOOGLE_GENERATIVE_AI_API_KEY"), + cronSchedule: assertEnv("CRON_SCHEDULE"), + maxNewsPerFetch: Number(optionalEnv("MAX_NEWS_PER_FETCH") ?? 50), +}; From 06f22a7504bc253fdb1e64de7bfbdc0d6943a16f Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sat, 4 Apr 2026 17:24:17 +0900 Subject: [PATCH 03/11] feat: store news state for duplicated --- servers/geek-news-bot/src/store.ts | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 servers/geek-news-bot/src/store.ts diff --git a/servers/geek-news-bot/src/store.ts b/servers/geek-news-bot/src/store.ts new file mode 100644 index 0000000..4583e67 --- /dev/null +++ b/servers/geek-news-bot/src/store.ts @@ -0,0 +1,33 @@ +import path from "path"; +import { fileURLToPath } from "url"; +import fsp from "fs/promises"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); +const STORE_PATH = path.relative(dirname, "./store.json"); + +interface NewsState { + lastPublishedAt: string; + recentIds: string[]; +} + +export const loadStates = async () => { + try { + const raw = await fsp.readFile(STORE_PATH, "utf-8"); + return JSON.parse(raw) as NewsState; + } catch (error) { + console.error(error); + return { + lastPublishedAt: "", + recentIds: [], + }; + } +}; + +export const saveStates = async (lastPublishedAt: string, ids: string[]) => { + const state: NewsState = { + lastPublishedAt, + recentIds: ids, + }; + + await fsp.writeFile(STORE_PATH, JSON.stringify(state, null, 2)); +}; From d561a96fe2f8c855f42ef554844a6925b9873889 Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sat, 4 Apr 2026 17:24:36 +0900 Subject: [PATCH 04/11] feat: ai sdk filtering --- servers/geek-news-bot/src/filter.ts | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 servers/geek-news-bot/src/filter.ts diff --git a/servers/geek-news-bot/src/filter.ts b/servers/geek-news-bot/src/filter.ts new file mode 100644 index 0000000..8d79675 --- /dev/null +++ b/servers/geek-news-bot/src/filter.ts @@ -0,0 +1,70 @@ +import { generateObject } from "ai"; +import { google } from "@ai-sdk/google"; +import { z } from "zod"; +import dedent from "dedent"; +import type { GeekNewsItem } from "./rss"; + +const FilterResultSchema = z.object({ + results: z.array( + z.object({ + id: z.string(), + isRelevant: z.boolean(), + reason: z.string(), + }), + ), +}); + +export interface FilteredNewsItem extends GeekNewsItem { + reason: string; +} + +export const filterFrontendNews = async (items: GeekNewsItem[]): Promise => { + if (items.length === 0) return []; + + const newsList = items.map((item) => `- [${item.id}] ${item.title}: ${item.content.slice(0, 200)}`).join("\n"); + + const { object } = await generateObject({ + model: google("gemini-2.5-flash"), + schema: FilterResultSchema, + prompt: dedent` + 당신은 프론트엔드 개발자 Slack 채널의 뉴스 큐레이터입니다. + + 아래 GeekNews 기사들을 분석하여 프론트엔드 개발자에게 관련 있는 기사를 판별해주세요. + + 관련 있는 주제 (우선순위 높음 → 낮음): + - [최우선] AI/LLM 관련: AI 코딩 어시스턴트, LLM 기반 개발 도구, AI SDK, 프론트엔드에서의 AI 활용 (on-device AI, WebGPU, AI UX 패턴 등), MCP, AI 에이전트, 주요 AI 모델 출시/업데이트 + - [최우선] AI와 개발 생산성: Copilot, Cursor, v0, Vercel AI SDK 등 AI 기반 DX 도구, 프롬프트 엔지니어링, AI 코드 리뷰 + - JavaScript, TypeScript 및 관련 생태계 + - React, Vue, Svelte, Angular, Next.js, Nuxt, Astro 등 프론트엔드 프레임워크 + - CSS, HTML, Web API, 브라우저 기술 + - 프론트엔드 도구 (번들러, 린터, 테스트 도구: Vite, Webpack, ESLint, Biome, Playwright 등) + - UI/UX 디자인 시스템, 웹 접근성(a11y) + - 웹 성능 최적화 + - 프론트엔드 아키텍처 패턴 + - Node.js / Deno / Bun 런타임 업데이트 (프론트엔드와 관련된 경우) + - 개발자 경험(DX) 도구 및 워크플로우 + - 주요 테크 기업의 프론트엔드 관련 엔지니어링 블로그 포스트 + + 관련 없는 주제: + - 순수 백엔드/인프라 주제 (데이터베이스, Kubernetes, 클라우드 운영) + - 크로스 플랫폼(React Native, Flutter)이 아닌 모바일 네이티브 개발 (Swift, Kotlin) + - 하드웨어, 보안 취약점, 순수 데이터 과학/ML 연구 + - 기술적 내용이 없는 비즈니스/스타트업 뉴스 + + 뉴스 기사 목록: + ${newsList} + + 각 기사에 대해 isRelevant를 true/false로 설정하고, 간결한 한국어 사유를 작성해주세요. + `, + }); + + const relevantIds = new Set(object.results.filter((r) => r.isRelevant).map((r) => r.id)); + const reasonMap = new Map(object.results.map((r) => [r.id, r.reason])); + + return items + .filter((item) => relevantIds.has(item.id)) + .map((item) => ({ + ...item, + reason: reasonMap.get(item.id) ?? "", + })); +}; From 999fdaa37f7e3da38372e25e3bc170d0f4206f94 Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sat, 4 Apr 2026 17:25:01 +0900 Subject: [PATCH 05/11] feat: send slack blocks --- servers/geek-news-bot/src/slack.ts | 67 ++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 servers/geek-news-bot/src/slack.ts diff --git a/servers/geek-news-bot/src/slack.ts b/servers/geek-news-bot/src/slack.ts new file mode 100644 index 0000000..d4b43e4 --- /dev/null +++ b/servers/geek-news-bot/src/slack.ts @@ -0,0 +1,67 @@ +import { slackClient } from "@makers-devops/slack/client"; +import type { KnownBlock } from "@slack/types"; +import type { FilteredNewsItem } from "./filter"; +import { ENV } from "./env"; + +const MAX_SLACK_ITEMS = 15; + +const buildNewsBlocks = (items: FilteredNewsItem[]): KnownBlock[] => { + const capped = items.slice(0, MAX_SLACK_ITEMS); + const blocks: KnownBlock[] = [ + { + type: "header", + text: { + type: "plain_text", + text: "GeekNews - Frontend 관련 뉴스", + }, + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: `AI가 선별한 Frontend 관련 뉴스 *${capped.length}건*이 있습니다.`, + }, + ], + }, + { type: "divider" }, + ]; + + for (const item of capped) { + blocks.push( + { + type: "section", + text: { + type: "mrkdwn", + text: `*<${item.link}|${item.title}>*\n${item.reason}`, + }, + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: `${item.pubDate}`, + }, + ], + }, + { type: "divider" }, + ); + } + + return blocks; +}; + +export const sendSlackNotification = async (items: FilteredNewsItem[]) => { + if (items.length === 0) { + return; + } + + const blocks = buildNewsBlocks(items); + + await slackClient.chat.postMessage({ + channel: ENV.slackChannelId, + text: `GeekNews Frontend 관련 뉴스 ${items.length}건`, + blocks, + }); +}; From 4305b01dd9fef1e94a9a1c4302eb66f618210270 Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sat, 4 Apr 2026 17:25:38 +0900 Subject: [PATCH 06/11] feat: set cron schedule for sending news --- pnpm-lock.yaml | 914 ++++++++++++++++++++++++++++ servers/geek-news-bot/package.json | 32 + servers/geek-news-bot/src/fetch.ts | 48 ++ servers/geek-news-bot/src/index.ts | 19 + servers/geek-news-bot/src/rss.ts | 26 + servers/geek-news-bot/tsconfig.json | 13 + 6 files changed, 1052 insertions(+) create mode 100644 servers/geek-news-bot/package.json create mode 100644 servers/geek-news-bot/src/fetch.ts create mode 100644 servers/geek-news-bot/src/index.ts create mode 100644 servers/geek-news-bot/src/rss.ts create mode 100644 servers/geek-news-bot/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16317fd..b1f7e8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,49 @@ importers: specifier: ^2.19.0 version: 2.20.0 + servers/geek-news-bot: + dependencies: + '@ai-sdk/google': + specifier: ^3.0.57 + version: 3.0.57(zod@3.25.76) + '@makers-devops/shared': + specifier: workspace:* + version: link:../../packages/shared + '@makers-devops/slack': + specifier: workspace:* + version: link:../../packages/slack + ai: + specifier: ^6.0.145 + version: 6.0.145(zod@3.25.76) + dedent: + specifier: ^1.7.2 + version: 1.7.2 + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + node-cron: + specifier: ^3.0.3 + version: 3.0.3 + rss-parser: + specifier: ^3.13.0 + version: 3.13.0 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@slack/types': + specifier: ^2.19.0 + version: 2.20.0 + '@types/node-cron': + specifier: ^3.0.11 + version: 3.0.11 + vitest: + specifier: ^4.1.2 + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.3.2)(esbuild@0.27.3)(tsx@4.21.0)(yaml@2.8.2)) + servers/mumu: dependencies: '@makers-devops/figma': @@ -128,6 +171,28 @@ importers: packages: + '@ai-sdk/gateway@3.0.87': + resolution: {integrity: sha512-knLx/VY0u5KAZGgrTorWCTbEnwK3oCCdm8yjxVQm3s14erqVo60SP08dsFWm+xNULPTusftQGVD/l0/hx5QOHg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@3.0.57': + resolution: {integrity: sha512-ojbYWB1loG8EGdfOWCbbtasoSwfjsGbOt2I1uPBLmoIBOa7EtUuYP4SujxHADZJFg2h08oUt4Dcw+1hJsjlF6A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.22': + resolution: {integrity: sha512-B2OTFcRw/Pdka9ZTjpXv6T6qZ6RruRuLokyb8HwW+aoW9ndJ3YasA3/mVswyJw7VMBF8ofXgqvcrCt9KYvFifg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + '@biomejs/biome@2.4.4': resolution: {integrity: sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q==} engines: {node: '>=14.21.3'} @@ -181,6 +246,15 @@ packages: cpu: [x64] os: [win32] + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -337,6 +411,15 @@ packages: cpu: [x64] os: [win32] + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@octokit/auth-token@5.1.2': resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} engines: {node: '>= 18'} @@ -395,6 +478,105 @@ packages: '@octokit/types@14.1.0': resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@slack/logger@4.0.0': resolution: {integrity: sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==} engines: {node: '>= 18', npm: '>= 8.6.0'} @@ -407,15 +589,30 @@ packages: resolution: {integrity: sha512-RoygyteJeFswxDPJjUMESn9dldWVMD2xUcHHd9DenVavSfVC6FeVnSdDerOO7m8LLvw4Q132nQM4hX8JiF7dng==} engines: {node: '>= 18', npm: '>= 8.6.0'} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} @@ -425,6 +622,9 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/node-cron@3.0.11': + resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} + '@types/node@25.3.2': resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} @@ -446,10 +646,49 @@ packages: '@upstash/redis@1.36.3': resolution: {integrity: sha512-wxo1ei4OHDHm4UGMgrNVz9QUEela9N/Iwi4p1JlHNSowQiPi+eljlGnfbZVkV0V4PIrjGtGFJt5GjWM5k28enA==} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + ai@6.0.145: + resolution: {integrity: sha512-RbMiFsPZxE4uf5Hhs8rscp5bIwvjQOrqS5dQGWNVRHGM947QZgkKX7Ih5hto8MK/7xkbtneoOZruZ8oSLO828A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -465,6 +704,10 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -494,6 +737,10 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -521,6 +768,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -540,6 +790,14 @@ packages: supports-color: optional: true + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -552,6 +810,10 @@ packages: 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'} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -570,6 +832,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -582,6 +847,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -598,6 +866,9 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -608,6 +879,14 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} @@ -615,6 +894,15 @@ packages: fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -718,6 +1006,79 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lint-staged@16.3.1: resolution: {integrity: sha512-bqvvquXzFBAlSbluugR4KXAe4XnO/QZcKVszpkBtqLWa2KEiVy8n6Xp38OeUbv/gOJOX4Vo9u5pFt/ADvbm42Q==} engines: {node: '>=20.17'} @@ -731,6 +1092,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -773,10 +1137,19 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + node-cron@3.0.3: + resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} + engines: {node: '>=6.0.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -785,6 +1158,9 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -816,10 +1192,24 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -853,12 +1243,24 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rss-parser@3.13.0: + resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + send@0.19.2: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} @@ -886,6 +1288,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -898,10 +1303,20 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -918,10 +1333,21 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -930,6 +1356,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -961,24 +1390,146 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} snapshots: + '@ai-sdk/gateway@3.0.87(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.22(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + + '@ai-sdk/google@3.0.57(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.22(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.22(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + '@biomejs/biome@2.4.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.4.4 @@ -1014,6 +1565,22 @@ snapshots: '@biomejs/cli-win32-x64@2.4.4': optional: true + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.27.3': optional: true @@ -1092,6 +1659,15 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@octokit/auth-token@5.1.2': {} '@octokit/core@6.1.6': @@ -1160,6 +1736,62 @@ snapshots: dependencies: '@octokit/openapi-types': 25.1.0 + '@opentelemetry/api@1.9.0': {} + + '@oxc-project/types@0.122.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + '@slack/logger@4.0.0': dependencies: '@types/node': 25.3.2 @@ -1183,11 +1815,23 @@ snapshots: transitivePeerDependencies: - debug + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 '@types/node': 25.3.2 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/connect@3.4.38': dependencies: '@types/node': 25.3.2 @@ -1196,6 +1840,10 @@ snapshots: dependencies: '@types/node': 25.3.2 + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.1': dependencies: '@types/node': 25.3.2 @@ -1211,6 +1859,8 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/node-cron@3.0.11': {} + '@types/node@25.3.2': dependencies: undici-types: 7.18.2 @@ -1234,11 +1884,62 @@ snapshots: dependencies: uncrypto: 0.1.3 + '@vercel/oidc@3.1.0': {} + + '@vitest/expect@4.1.2': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.2(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.3.2)(esbuild@0.27.3)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.3.2)(esbuild@0.27.3)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/pretty-format@4.1.2': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.2': + dependencies: + '@vitest/utils': 4.1.2 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.2': {} + + '@vitest/utils@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 + ai@6.0.145(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.87(zod@3.25.76) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.22(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -1249,6 +1950,8 @@ snapshots: array-flatten@1.1.1: {} + assertion-error@2.0.1: {} + asynckit@0.4.0: {} axios@1.13.5: @@ -1294,6 +1997,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + chai@6.2.2: {} + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -1317,6 +2022,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.0.7: {} cookie@0.7.2: {} @@ -1330,12 +2037,16 @@ snapshots: dependencies: ms: 2.0.0 + dedent@1.7.2: {} + delayed-stream@1.0.0: {} depd@2.0.0: {} destroy@1.2.0: {} + detect-libc@2.1.2: {} + dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -1350,12 +2061,16 @@ snapshots: encodeurl@2.0.0: {} + entities@2.2.0: {} + environment@1.1.0: {} es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -1398,12 +2113,20 @@ snapshots: escape-html@1.0.3: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + etag@1.8.1: {} eventemitter3@4.0.7: {} eventemitter3@5.0.4: {} + eventsource-parser@3.0.6: {} + + expect-type@1.3.0: {} + express@4.22.1: dependencies: accepts: 1.3.8 @@ -1442,6 +2165,10 @@ snapshots: fast-content-type-parse@2.0.1: {} + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -1541,6 +2268,57 @@ snapshots: is-stream@2.0.1: {} + json-schema@0.4.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lint-staged@16.3.1: dependencies: commander: 14.0.3 @@ -1567,6 +2345,10 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} media-typer@0.3.0: {} @@ -1594,12 +2376,20 @@ snapshots: ms@2.1.3: {} + nanoid@3.3.11: {} + negotiator@0.6.3: {} + node-cron@3.0.3: + dependencies: + uuid: 8.3.2 + object-assign@4.1.1: {} object-inspect@1.13.4: {} + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -1628,8 +2418,20 @@ snapshots: path-to-regexp@0.1.12: {} + pathe@2.0.3: {} + + picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.4: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -1661,10 +2463,41 @@ snapshots: rfdc@1.4.1: {} + rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + rss-parser@3.13.0: + dependencies: + entities: 2.2.0 + xml2js: 0.5.0 + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} + sax@1.6.0: {} + send@0.19.2: dependencies: debug: 2.6.9 @@ -1722,6 +2555,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} slice-ansi@7.1.2: @@ -1734,8 +2569,14 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.0.0: {} + string-argv@0.3.2: {} string-width@7.2.0: @@ -1753,14 +2594,26 @@ snapshots: dependencies: ansi-regex: 6.2.2 + tinybench@2.9.0: {} + tinyexec@1.0.2: {} + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 toidentifier@1.0.1: {} + tslib@2.8.1: + optional: true + tsx@4.21.0: dependencies: esbuild: 0.27.3 @@ -1785,14 +2638,75 @@ snapshots: utils-merge@1.0.1: {} + uuid@8.3.2: {} + vary@1.1.2: {} + vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.3.2)(esbuild@0.27.3)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.3.2 + esbuild: 0.27.3 + fsevents: 2.3.3 + tsx: 4.21.0 + yaml: 2.8.2 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.3.2)(esbuild@0.27.3)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.3.2)(esbuild@0.27.3)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.3.2)(esbuild@0.27.3)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 25.3.2 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 string-width: 7.2.0 strip-ansi: 7.2.0 + xml2js@0.5.0: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + yaml@2.8.2: {} + zod@3.25.76: {} + zod@4.3.6: {} diff --git a/servers/geek-news-bot/package.json b/servers/geek-news-bot/package.json new file mode 100644 index 0000000..2d97706 --- /dev/null +++ b/servers/geek-news-bot/package.json @@ -0,0 +1,32 @@ +{ + "name": "@makers-devops/geek-news-bot", + "version": "1.0.0", + "type": "module", + "private": true, + "main": "index.js", + "scripts": { + "dev": "tsx --watch src/index.ts", + "start": "tsx src/index.ts", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@ai-sdk/google": "^3.0.57", + "@makers-devops/shared": "workspace:*", + "@makers-devops/slack": "workspace:*", + "ai": "^6.0.145", + "dedent": "^1.7.2", + "dotenv": "^16.4.0", + "node-cron": "^3.0.3", + "rss-parser": "^3.13.0", + "tsx": "^4.19.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@slack/types": "^2.19.0", + "@types/node-cron": "^3.0.11", + "vitest": "^4.1.2" + }, + "packageManager": "pnpm@10.22.0" +} diff --git a/servers/geek-news-bot/src/fetch.ts b/servers/geek-news-bot/src/fetch.ts new file mode 100644 index 0000000..35c20f2 --- /dev/null +++ b/servers/geek-news-bot/src/fetch.ts @@ -0,0 +1,48 @@ +import { fetchGeekNews } from "./rss"; +import { filterFrontendNews } from "./filter"; +import { sendSlackNotification } from "./slack"; +import { loadStates, saveStates } from "./store"; +import { ENV } from "./env"; + +export const runNewsJob = async () => { + console.log(`[${new Date().toISOString()}] 긱뉴스 패치 시작`); + + const allNews = await fetchGeekNews(); + console.log(`긱뉴스 RSS에서 ${allNews.length}건의 뉴스를 패치했습니다`); + + const state = await loadStates(); + const recentIdSet = new Set(state.recentIds); + + const newItems = allNews + .filter((item) => { + /** 타임스탬프 기반 필터링 */ + if (state.lastPublishedAt && item.pubDate <= state.lastPublishedAt) { + /** 동일 시각 발행 뉴스는 ID로 중복 체크 */ + if (item.pubDate === state.lastPublishedAt) { + return !recentIdSet.has(item.id); + } + return false; + } + return true; + }) + .slice(0, ENV.maxNewsPerFetch); + + if (newItems.length === 0) { + console.log("새로운 뉴스가 없습니다."); + return; + } + + /** AI 필터링 */ + const frontendNews = await filterFrontendNews(newItems); + + await sendSlackNotification(frontendNews); + + const allProcessedIds = [...state.recentIds, ...newItems.map((item) => item.id)]; + const latestPubDate = newItems.reduce( + (latest, item) => (item.pubDate > latest ? item.pubDate : latest), + state.lastPublishedAt, + ); + await saveStates(latestPubDate, allProcessedIds); + + console.log(`[${new Date().toISOString()}] 작업 완료`); +}; diff --git a/servers/geek-news-bot/src/index.ts b/servers/geek-news-bot/src/index.ts new file mode 100644 index 0000000..1e058b1 --- /dev/null +++ b/servers/geek-news-bot/src/index.ts @@ -0,0 +1,19 @@ +import cron from "node-cron"; +import { runNewsJob } from "./fetch"; +import { ENV } from "./env"; + +process.on("unhandledRejection", (err) => { + console.error("Unhandled rejection:", err); +}); + +/** 최초 fetch */ +runNewsJob().catch((err) => { + console.error("Initial run failed:", err); +}); + +/** 정기 fetch */ +cron.schedule(ENV.cronSchedule, () => { + runNewsJob().catch((err) => { + console.error("Cron Schedule failed:", err); + }); +}); diff --git a/servers/geek-news-bot/src/rss.ts b/servers/geek-news-bot/src/rss.ts new file mode 100644 index 0000000..9043e76 --- /dev/null +++ b/servers/geek-news-bot/src/rss.ts @@ -0,0 +1,26 @@ +import Parser from "rss-parser"; + +export interface GeekNewsItem { + id: string; + title: string; + link: string; + pubDate: string; + content: string; +} +const parser = new Parser(); + +const GEEK_NEWS_RSS_URL = "https://news.hada.io/rss/news"; + +export const fetchGeekNews = async (): Promise => { + const feed = await parser.parseURL(GEEK_NEWS_RSS_URL); + + return feed.items.map( + (item): GeekNewsItem => ({ + id: item.guid ?? "", + title: item.title ?? "", + link: item.link ?? "", + pubDate: item.pubDate ?? "", + content: item.content ?? "", + }), + ); +}; diff --git a/servers/geek-news-bot/tsconfig.json b/servers/geek-news-bot/tsconfig.json new file mode 100644 index 0000000..88d6010 --- /dev/null +++ b/servers/geek-news-bot/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"] +} From 4c57a3d127b2849c2741729f49655dc73e4e42c9 Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sat, 4 Apr 2026 17:25:50 +0900 Subject: [PATCH 07/11] feat: pipeline test --- servers/geek-news-bot/src/__tests__/data.ts | 43 ++++++++++++ .../src/__tests__/pipeline.test.ts | 68 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 servers/geek-news-bot/src/__tests__/data.ts create mode 100644 servers/geek-news-bot/src/__tests__/pipeline.test.ts diff --git a/servers/geek-news-bot/src/__tests__/data.ts b/servers/geek-news-bot/src/__tests__/data.ts new file mode 100644 index 0000000..22aac54 --- /dev/null +++ b/servers/geek-news-bot/src/__tests__/data.ts @@ -0,0 +1,43 @@ +import type { GeekNewsItem } from "../rss"; + +export const mockNewsItems: GeekNewsItem[] = [ + { + id: "mock-1", + title: "React 25 릴리즈 - React Compiler 정식 포함", + link: "https://example.com/react-25", + pubDate: "2026-04-01", + content: + "React 25가 정식 릴리즈되었습니다. React Compiler가 안정화되어 기본 포함되며, 자동 메모이제이션으로 성능이 크게 향상됩니다.", + }, + { + id: "mock-2", + title: "PostgreSQL 18 출시 - 새로운 파티셔닝 전략", + link: "https://example.com/postgresql-18", + pubDate: "2026-04-01", + content: + "PostgreSQL 18이 출시되었습니다. 새로운 자동 파티셔닝 전략과 향상된 쿼리 플래너가 포함되어 대규모 데이터 처리 성능이 개선됩니다.", + }, + { + id: "mock-3", + title: "Vite 7.0 발표 - Rolldown 기반 번들링 엔진", + link: "https://example.com/vite-7", + pubDate: "2026-04-02", + content: + "Vite 7.0이 발표되었습니다. Rolldown 기반의 새로운 번들링 엔진으로 빌드 속도가 10배 빨라졌으며, HMR 성능도 대폭 개선되었습니다.", + }, + { + id: "mock-4", + title: "쿠버네티스 1.34 릴리즈 - Sidecar 컨테이너 GA", + link: "https://example.com/k8s-134", + pubDate: "2026-04-02", + content: "Kubernetes 1.34가 릴리즈되었습니다. Sidecar 컨테이너가 GA되었고, 새로운 스케줄링 기능이 추가되었습니다.", + }, + { + id: "mock-5", + title: "Cursor AI 에이전트 모드 업데이트 - 멀티파일 편집 강화", + link: "https://example.com/cursor-agent", + pubDate: "2026-04-03", + content: + "Cursor의 AI 에이전트 모드가 대폭 업데이트되었습니다. 멀티파일 동시 편집, 자동 테스트 생성 등 개발 생산성을 높이는 기능이 추가되었습니다.", + }, +]; diff --git a/servers/geek-news-bot/src/__tests__/pipeline.test.ts b/servers/geek-news-bot/src/__tests__/pipeline.test.ts new file mode 100644 index 0000000..618d072 --- /dev/null +++ b/servers/geek-news-bot/src/__tests__/pipeline.test.ts @@ -0,0 +1,68 @@ +import "dotenv/config"; +import { describe, it, expect } from "vitest"; +import { fetchGeekNews } from "../rss"; +import { mockNewsItems } from "./data"; +import { filterFrontendNews } from "../filter"; +import { sendSlackNotification } from "../slack"; + +const hasValidGoogleKey = !!process.env.GOOGLE_GENERATIVE_AI_API_KEY; + +const FRONTEND_MOCK_IDS = new Set(["mock-1", "mock-3", "mock-5"]); + +describe("geek-news-bot 파이프라인 통합 테스트", () => { + it("GeekNews RSS에서 뉴스를 정상적으로 패치한다", async () => { + const items = await fetchGeekNews(); + + expect(items.length).toBeGreaterThan(0); + + for (const item of items) { + expect(item.title).toBeTruthy(); + expect(item.link).toBeTruthy(); + } + + console.log(`[RSS] ${items.length}건 패치 완료`); + console.log(`[RSS] 첫 번째 항목: ${items[0].title}`); + }, 10_000); + + it.skipIf(!hasValidGoogleKey)( + "AI 필터링이 관련 뉴스를 올바르게 분류한다", + async () => { + const filtered = await filterFrontendNews(mockNewsItems); + + expect(filtered).toBeDefined(); + expect(Array.isArray(filtered)).toBe(true); + expect(filtered.length).toBeGreaterThan(0); + + for (const item of filtered) { + expect(item.reason).toBeTruthy(); + expect(item.title).toBeTruthy(); + expect(FRONTEND_MOCK_IDS.has(item.id)).toBe(true); + } + + const filteredIds = new Set(filtered.map((item) => item.id)); + expect(filteredIds.has("mock-2")).toBe(false); + expect(filteredIds.has("mock-4")).toBe(false); + + console.log(`[AI 필터] ${mockNewsItems.length}건 중 ${filtered.length}건 선별`); + for (const item of filtered) { + console.log(` - [선별] ${item.title}: ${item.reason}`); + } + }, + 30_000, + ); + + it.skipIf(!hasValidGoogleKey)( + "전체 파이프라인: mock 뉴스 → AI 필터링 → 슬랙 전송", + async () => { + const filtered = await filterFrontendNews(mockNewsItems); + + console.log(`[파이프라인] ${mockNewsItems.length}건 중 ${filtered.length}건 프론트엔드 관련 뉴스 선별`); + + expect(filtered.length).toBeGreaterThan(0); + + await sendSlackNotification(filtered); + console.log("[파이프라인] 슬랙 전송 완료"); + }, + 60_000, + ); +}); From c61d8f7aee3463fcc9f06e607ffcdd6d91db4b75 Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sat, 4 Apr 2026 17:33:19 +0900 Subject: [PATCH 08/11] feat: dockerfile --- servers/geek-news-bot/Dockerfile | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 servers/geek-news-bot/Dockerfile diff --git a/servers/geek-news-bot/Dockerfile b/servers/geek-news-bot/Dockerfile new file mode 100644 index 0000000..cd5ca2c --- /dev/null +++ b/servers/geek-news-bot/Dockerfile @@ -0,0 +1,25 @@ +FROM node:22-alpine AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +FROM base AS deps +WORKDIR /app + +COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ +COPY packages/ ./packages/ +COPY servers/geek-news-bot/package.json ./servers/geek-news-bot/package.json + +RUN pnpm install --frozen-lockfile + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production + +COPY --from=deps /app ./ + +COPY packages/ ./packages/ +COPY servers/geek-news-bot ./servers/geek-news-bot + +EXPOSE 8000 +CMD ["pnpm", "-F=@makers-devops/geek-news-bot", "start"] From 85785595265301be7dac556749df5981632c3659 Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sun, 5 Apr 2026 01:40:47 +0900 Subject: [PATCH 09/11] feat: store state by redis --- servers/geek-news-bot/.env.example | 4 +++- servers/geek-news-bot/Dockerfile | 1 - servers/geek-news-bot/package.json | 1 + servers/geek-news-bot/src/store.ts | 27 +++++++++++++-------------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/servers/geek-news-bot/.env.example b/servers/geek-news-bot/.env.example index fa4cbcf..06210bc 100644 --- a/servers/geek-news-bot/.env.example +++ b/servers/geek-news-bot/.env.example @@ -2,4 +2,6 @@ SLACK_BOT_TOKEN= SLACK_CHANNEL_ID= GOOGLE_GENERATIVE_AI_API_KEY= CRON_SCHEDULE= -MAX_NEWS_PER_FETCH= \ No newline at end of file +MAX_NEWS_PER_FETCH= +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= \ No newline at end of file diff --git a/servers/geek-news-bot/Dockerfile b/servers/geek-news-bot/Dockerfile index cd5ca2c..66375fb 100644 --- a/servers/geek-news-bot/Dockerfile +++ b/servers/geek-news-bot/Dockerfile @@ -21,5 +21,4 @@ COPY --from=deps /app ./ COPY packages/ ./packages/ COPY servers/geek-news-bot ./servers/geek-news-bot -EXPOSE 8000 CMD ["pnpm", "-F=@makers-devops/geek-news-bot", "start"] diff --git a/servers/geek-news-bot/package.json b/servers/geek-news-bot/package.json index 2d97706..4a9bb82 100644 --- a/servers/geek-news-bot/package.json +++ b/servers/geek-news-bot/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@ai-sdk/google": "^3.0.57", + "@makers-devops/redis": "workspace:*", "@makers-devops/shared": "workspace:*", "@makers-devops/slack": "workspace:*", "ai": "^6.0.145", diff --git a/servers/geek-news-bot/src/store.ts b/servers/geek-news-bot/src/store.ts index 4583e67..bd75066 100644 --- a/servers/geek-news-bot/src/store.ts +++ b/servers/geek-news-bot/src/store.ts @@ -1,25 +1,24 @@ -import path from "path"; -import { fileURLToPath } from "url"; -import fsp from "fs/promises"; +import { redisClient } from "@makers-devops/redis"; -const dirname = path.dirname(fileURLToPath(import.meta.url)); -const STORE_PATH = path.relative(dirname, "./store.json"); +const REDIS_KEY = "geek-news-state"; interface NewsState { lastPublishedAt: string; recentIds: string[]; } -export const loadStates = async () => { +const DEFAULT_STATE: NewsState = { + lastPublishedAt: "", + recentIds: [], +}; + +export const loadStates = async (): Promise => { try { - const raw = await fsp.readFile(STORE_PATH, "utf-8"); - return JSON.parse(raw) as NewsState; + const state = await redisClient.get(REDIS_KEY); + return state ?? DEFAULT_STATE; } catch (error) { - console.error(error); - return { - lastPublishedAt: "", - recentIds: [], - }; + console.error("[store] Redis 조회 실패:", error); + return DEFAULT_STATE; } }; @@ -29,5 +28,5 @@ export const saveStates = async (lastPublishedAt: string, ids: string[]) => { recentIds: ids, }; - await fsp.writeFile(STORE_PATH, JSON.stringify(state, null, 2)); + await redisClient.set(REDIS_KEY, state); }; From 22700e9e4f4ff9083607b8b2b7a1607aae407fe5 Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sun, 5 Apr 2026 16:06:30 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20pub=20date=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=20=EB=B9=84=EA=B5=90,=20id=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 3 +++ servers/geek-news-bot/src/fetch.ts | 31 ++++++++++++++++++++---------- servers/geek-news-bot/src/store.ts | 31 +++++++++++++++++++++++------- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1f7e8c..574b5e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,9 @@ importers: '@ai-sdk/google': specifier: ^3.0.57 version: 3.0.57(zod@3.25.76) + '@makers-devops/redis': + specifier: workspace:* + version: link:../../packages/redis '@makers-devops/shared': specifier: workspace:* version: link:../../packages/shared diff --git a/servers/geek-news-bot/src/fetch.ts b/servers/geek-news-bot/src/fetch.ts index 35c20f2..b1f4f2a 100644 --- a/servers/geek-news-bot/src/fetch.ts +++ b/servers/geek-news-bot/src/fetch.ts @@ -4,6 +4,11 @@ import { sendSlackNotification } from "./slack"; import { loadStates, saveStates } from "./store"; import { ENV } from "./env"; +const toTimestamp = (dateStr: string): number => { + const ms = new Date(dateStr).getTime(); + return Number.isNaN(ms) ? 0 : ms; +}; + export const runNewsJob = async () => { console.log(`[${new Date().toISOString()}] 긱뉴스 패치 시작`); @@ -15,10 +20,15 @@ export const runNewsJob = async () => { const newItems = allNews .filter((item) => { - /** 타임스탬프 기반 필터링 */ - if (state.lastPublishedAt && item.pubDate <= state.lastPublishedAt) { - /** 동일 시각 발행 뉴스는 ID로 중복 체크 */ - if (item.pubDate === state.lastPublishedAt) { + const itemTs = toTimestamp(item.pubDate); + + /** + * 타임스탬프 기반 중복 필터링 + * - lastPublishedAt보다 오래된 뉴스는 이미 처리된 것으로 간주 + * - lastPublishedAt와 동일한 타임스탬프에 발행된 뉴스는 recentIds로 중복 체크 + */ + if (state.lastPublishedAt && itemTs <= state.lastPublishedAt) { + if (itemTs === state.lastPublishedAt) { return !recentIdSet.has(item.id); } return false; @@ -32,17 +42,18 @@ export const runNewsJob = async () => { return; } - /** AI 필터링 */ const frontendNews = await filterFrontendNews(newItems); - await sendSlackNotification(frontendNews); - + /** 다음 실행의 중복 방지를 위해 처리된 뉴스 ID를 누적 */ const allProcessedIds = [...state.recentIds, ...newItems.map((item) => item.id)]; - const latestPubDate = newItems.reduce( - (latest, item) => (item.pubDate > latest ? item.pubDate : latest), + /** 최신 발생 시간 타임스탬프 기준 최신화 */ + const latestTimestamp = newItems.reduce( + (latest, item) => Math.max(latest, toTimestamp(item.pubDate)), state.lastPublishedAt, ); - await saveStates(latestPubDate, allProcessedIds); + await saveStates(latestTimestamp, allProcessedIds); + + await sendSlackNotification(frontendNews); console.log(`[${new Date().toISOString()}] 작업 완료`); }; diff --git a/servers/geek-news-bot/src/store.ts b/servers/geek-news-bot/src/store.ts index bd75066..ec5f4a1 100644 --- a/servers/geek-news-bot/src/store.ts +++ b/servers/geek-news-bot/src/store.ts @@ -1,32 +1,49 @@ import { redisClient } from "@makers-devops/redis"; const REDIS_KEY = "geek-news-state"; +const MAX_RECENT_IDS = 100; interface NewsState { - lastPublishedAt: string; + lastPublishedAt: number; recentIds: string[]; } const DEFAULT_STATE: NewsState = { - lastPublishedAt: "", + lastPublishedAt: 0, recentIds: [], }; export const loadStates = async (): Promise => { try { - const state = await redisClient.get(REDIS_KEY); - return state ?? DEFAULT_STATE; + const raw = await redisClient.get>(REDIS_KEY); + if (!raw) return DEFAULT_STATE; + + const lastPublishedAt = + typeof raw.lastPublishedAt === "string" + ? new Date(raw.lastPublishedAt).getTime() || 0 + : ((raw.lastPublishedAt as number) ?? 0); + + return { + lastPublishedAt, + recentIds: Array.isArray(raw.recentIds) ? raw.recentIds : [], + }; } catch (error) { console.error("[store] Redis 조회 실패:", error); return DEFAULT_STATE; } }; -export const saveStates = async (lastPublishedAt: string, ids: string[]) => { +export const saveStates = async (lastPublishedAt: number, ids: string[]) => { + /** MAX_RECENT_IDS 개수만 유지 */ const state: NewsState = { lastPublishedAt, - recentIds: ids, + recentIds: ids.slice(-MAX_RECENT_IDS), }; - await redisClient.set(REDIS_KEY, state); + try { + await redisClient.set(REDIS_KEY, state); + } catch (error) { + console.error("[store] Redis 저장 실패 — 다음 실행 시 중복 알림이 발생할 수 있습니다:", error); + throw error; + } }; From 03820505e08cc1239285d5b6fa1e4933fecb6f62 Mon Sep 17 00:00:00 2001 From: wuzoo Date: Sun, 5 Apr 2026 16:17:29 +0900 Subject: [PATCH 11/11] docs: rss feed analysis --- .../geek-news-bot/docs/rss-feed-analysis.md | 53 +++++++++++++++++++ servers/geek-news-bot/src/index.ts | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 servers/geek-news-bot/docs/rss-feed-analysis.md diff --git a/servers/geek-news-bot/docs/rss-feed-analysis.md b/servers/geek-news-bot/docs/rss-feed-analysis.md new file mode 100644 index 0000000..a3fbde7 --- /dev/null +++ b/servers/geek-news-bot/docs/rss-feed-analysis.md @@ -0,0 +1,53 @@ +# GeekNews RSS 피드 발행 빈도 분석 + +> 분석일: 2026-04-05 +> 분석 대상: https://news.hada.io/rss/news +> 목적: 현재 fetch 설정(3시간 주기 / 50건)으로 뉴스 유실 없이 안정적으로 운영 가능한지 검증 + +## 현재 설정 + +| 설정 | 값 | +| ------------------------------------ | ----- | +| 크론 주기 | 3시간 | +| 최대 fetch 수 (`MAX_NEWS_PER_FETCH`) | 50건 | +| RSS 피드 제공 상한 | 50건 | + +## 분석 결과 + +RSS 피드에서 제공하는 50건의 뉴스가 커버하는 기간과 발행 빈도를 분석했다. + +### 핵심 수치 + +| 지표 | 값 | +| -------------------- | ------------------ | +| 50건이 커버하는 기간 | 약 47.6시간 (~2일) | +| 시간당 평균 게시 수 | 1.05건 | +| 3시간당 평균 게시 수 | 3.2건 | +| 3시간당 최대 게시 수 | 12건 | +| 하루 평균 게시 수 | 25.2건 | + +### 3시간 윈도우별 게시 분포 + +``` +2026-04-04 09:00-12:00 KST: ████████████ 12건 (최대) +2026-04-05 09:00-12:00 KST: █████████ 9건 +2026-04-03 18:00-21:00 KST: █████ 5건 +2026-04-05 03:00-06:00 KST: ████ 4건 +나머지 윈도우: █~███ 1~3건 +``` + +### 일별 게시 수 + +``` +2026-04-05: ██████████ 10건 (13:00 기준, 집계 진행 중) +2026-04-04: ███████████████████████████ 27건 +2026-04-03: █████████████ 13건 +``` + +## 결론 + +현재 설정(3시간 주기 / 50건 fetch)은 실제 발행량 대비 **약 4~15배의 여유**가 있어 뉴스 유실 위험이 없다. + +- 3시간 동안 평균 3.2건, 최대 12건 게시 → 50건 한도에 도달할 가능성이 사실상 없음 +- RSS 피드 자체의 50건이 약 2일치를 커버하므로, 서버가 **24시간 다운되더라도** 하루 평균 25건으로 50건 한도 안에서 복구 가능 +- `MAX_NEWS_PER_FETCH` 또는 크론 주기를 별도로 조정할 필요 없음 diff --git a/servers/geek-news-bot/src/index.ts b/servers/geek-news-bot/src/index.ts index 1e058b1..4433cf4 100644 --- a/servers/geek-news-bot/src/index.ts +++ b/servers/geek-news-bot/src/index.ts @@ -6,7 +6,7 @@ process.on("unhandledRejection", (err) => { console.error("Unhandled rejection:", err); }); -/** 최초 fetch */ +/** 최초 fetch, 서버 시작 시만 유효 */ runNewsJob().catch((err) => { console.error("Initial run failed:", err); });