From bdb0052db58235cc838d1813b0af11a5de106bb3 Mon Sep 17 00:00:00 2001 From: aamoghS Date: Mon, 17 Aug 2026 16:51:40 +0000 Subject: [PATCH 1/4] Host @dsgt Slack bot on Firebase App Hosting Restore apps/dsgt-slack and serve Slack Events API, slash commands, and interactivity at /api/webhooks/slack on sites/mainweb. Production uses HTTP (not Socket Mode) with SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET from Secret Manager. --- README.md | 1 + apphosting.yaml | 13 + apps/dsgt-slack/.env.example | 22 + apps/dsgt-slack/README.md | 183 ++++ apps/dsgt-slack/eslint.config.mjs | 12 + apps/dsgt-slack/manifest.yaml | 39 + apps/dsgt-slack/package.json | 36 + apps/dsgt-slack/src/app.ts | 69 ++ apps/dsgt-slack/src/env.ts | 57 ++ apps/dsgt-slack/src/http.test.ts | 167 ++++ apps/dsgt-slack/src/http.ts | 211 ++++ apps/dsgt-slack/src/index.ts | 38 + apps/dsgt-slack/src/replies.test.ts | 42 + apps/dsgt-slack/src/replies.ts | 94 ++ apps/dsgt-slack/tsconfig.json | 9 + apps/dsgt-slack/turbo.json | 14 + apps/dsgt-slack/vitest.config.ts | 7 + docs/README.md | 6 + docs/architecture.md | 10 +- docs/getting-started.md | 4 +- docs/operations/deployment.md | 10 +- docs/operations/environment.md | 11 + docs/operations/testing.md | 8 +- docs/sites/mainweb.md | 3 +- package.json | 2 +- pnpm-lock.yaml | 898 +++++++++++++++++- .../app/(portal)/api/webhooks/slack/route.ts | 30 + sites/mainweb/next.config.mjs | 9 +- sites/mainweb/package.json | 1 + turbo.json | 6 +- 30 files changed, 2000 insertions(+), 12 deletions(-) create mode 100644 apps/dsgt-slack/.env.example create mode 100644 apps/dsgt-slack/README.md create mode 100644 apps/dsgt-slack/eslint.config.mjs create mode 100644 apps/dsgt-slack/manifest.yaml create mode 100644 apps/dsgt-slack/package.json create mode 100644 apps/dsgt-slack/src/app.ts create mode 100644 apps/dsgt-slack/src/env.ts create mode 100644 apps/dsgt-slack/src/http.test.ts create mode 100644 apps/dsgt-slack/src/http.ts create mode 100644 apps/dsgt-slack/src/index.ts create mode 100644 apps/dsgt-slack/src/replies.test.ts create mode 100644 apps/dsgt-slack/src/replies.ts create mode 100644 apps/dsgt-slack/tsconfig.json create mode 100644 apps/dsgt-slack/turbo.json create mode 100644 apps/dsgt-slack/vitest.config.ts create mode 100644 sites/mainweb/app/(portal)/api/webhooks/slack/route.ts diff --git a/README.md b/README.md index 78acdc68..073e7887 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Two Next.js sites share one Postgres database and four internal packages. Club m | --- | --- | --- | | `sites/mainweb` | `web` | Public club site + authenticated portal (App Hosting) | | `sites/hacklytics2027` | `hacklytics2027` | Hacklytics 2027 marketing site, static export (Firebase Hosting) | +| `apps/dsgt-slack` | `@query/dsgt-slack` | `@dsgt` Slack bot (HTTP webhook on mainweb in production) | | `packages/api` | `@query/api` | tRPC routers, middleware, pricing | | `packages/auth` | `@query/auth` | NextAuth (Google, GitHub, email codes) | | `packages/db` | `@query/db` | Drizzle schema, client, membership rules | diff --git a/apphosting.yaml b/apphosting.yaml index b987633f..0055fee0 100644 --- a/apphosting.yaml +++ b/apphosting.yaml @@ -57,6 +57,19 @@ env: secret: projects/672446353769/secrets/NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY - variable: STRIPE_WEBHOOK_SECRET secret: projects/672446353769/secrets/STRIPE_WEBHOOK_SECRET + # Slack Events API for @dsgt (HTTP, not Socket Mode). Create these secrets + # once before the deploy that references them, or App Hosting cannot start: + # gcloud secrets create SLACK_BOT_TOKEN --project=dsgt-website + # gcloud secrets create SLACK_SIGNING_SECRET --project=dsgt-website + # firebase apphosting:secrets:grantaccess SLACK_BOT_TOKEN --backend query + # firebase apphosting:secrets:grantaccess SLACK_SIGNING_SECRET --backend query + # Steps and Request URL: apps/dsgt-slack/README.md + - variable: SLACK_BOT_TOKEN + secret: projects/672446353769/secrets/SLACK_BOT_TOKEN + - variable: SLACK_SIGNING_SECRET + secret: projects/672446353769/secrets/SLACK_SIGNING_SECRET + - variable: SLACK_SOCKET_MODE + value: "false" - variable: NODE_ENV value: production # Consumer Gmail, which caps around 500 recipients a day — shared between diff --git a/apps/dsgt-slack/.env.example b/apps/dsgt-slack/.env.example new file mode 100644 index 00000000..a6ccd24a --- /dev/null +++ b/apps/dsgt-slack/.env.example @@ -0,0 +1,22 @@ +# Copy to .env and fill in values from https://api.slack.com/apps +# Never commit .env or real tokens. + +# Bot User OAuth Token (xoxb-...) from OAuth & Permissions after install +SLACK_BOT_TOKEN=xoxb-your-bot-token + +# Signing Secret from Basic Information +SLACK_SIGNING_SECRET=your-signing-secret + +# App-level token (xapp-...) from Basic Information → App-Level Tokens +# Required only for local Socket Mode. Create a token with the connections:write +# scope. Not used in production (HTTP Events API on Firebase App Hosting). +SLACK_APP_TOKEN=xapp-your-app-token + +# Local default is Socket Mode. Production App Hosting sets this to false. +# SLACK_APP_TOKEN is not required when this is false. +SLACK_SOCKET_MODE=true + +# Used only when running this package's local HTTP receiver +# (SLACK_SOCKET_MODE=false). Production Request URL is the App Hosting +# origin (AUTH_URL, host datasciencegt.org) plus /api/webhooks/slack. +PORT=3000 diff --git a/apps/dsgt-slack/README.md b/apps/dsgt-slack/README.md new file mode 100644 index 00000000..7aa1fe82 --- /dev/null +++ b/apps/dsgt-slack/README.md @@ -0,0 +1,183 @@ +# dsgt Slack bot + +Bolt for JavaScript app for **Data Science @ Georgia Tech (DS@GT)**. In Slack it +appears as **@dsgt**. This is a custom Slack app owned by the club, not a +third-party Slack integration. + +The bot answers `@dsgt` mentions, direct messages, and the `/dsgt` slash +command. It can confirm that it is online (`ping`) and tell people how to join: +the first event is **August 26 (8/26)**, further details will be announced +there, and the club site is datasciencegt.org. + +Production traffic is **HTTP Events API** on the existing Firebase App Hosting +backend (`sites/mainweb` / Cloud Run). There is no second Cloud Run service and +no Socket Mode process in production. + +## Create the Slack app from the manifest + +1. Open [https://api.slack.com/apps](https://api.slack.com/apps) and sign in to + the workspace that should host the bot (typically the DS@GT Slack). +2. Click **Create New App** → **From an app manifest**. +3. Select the workspace, then paste the contents of + [`manifest.yaml`](./manifest.yaml) (YAML is accepted). +4. Confirm the summary. The app name and bot user display name should both be + `dsgt`. Socket Mode should be **off**. Request URLs are left unset in the + manifest so you can create the app before the webhook is deployed. +5. Click **Create**. + +You now have a real Slack app whose bot user is **dsgt**. + +## Install into a workspace and collect tokens + +1. In the app settings sidebar, open **OAuth & Permissions** and click + **Install to Workspace**. Approve the requested bot scopes. +2. Copy the **Bot User OAuth Token** (`xoxb-...`). You will put this in GCP + Secret Manager as `SLACK_BOT_TOKEN` (and in `apps/dsgt-slack/.env` only if + you run the bot locally). +3. Open **Basic Information** and copy the **Signing Secret**. That is + `SLACK_SIGNING_SECRET`. + +Do **not** commit `.env`, tokens, or signing secrets. `SLACK_APP_TOKEN` is not +required in production. + +## Production Request URL (Firebase App Hosting) + +Slack must POST to the **Firebase App Hosting / Cloud Run** URL for `sites/mainweb` +(workspace `web`). That origin is `{AUTH_URL}` — the same public +origin as `AUTH_URL` / `NEXTAUTH_URL` in [`apphosting.yaml`](../../apphosting.yaml). + +**Use this Request URL everywhere** (Event Subscriptions, Slash Commands +`/dsgt`, Interactivity & Shortcuts). Take the `AUTH_URL` / `NEXTAUTH_URL` +value from [`apphosting.yaml`](../../apphosting.yaml) (the App Hosting origin +for the club site, host `datasciencegt.org`) and append the path: + +```text +{AUTH_URL}/api/webhooks/slack +``` + +Set those three URLs in the Slack app settings **after** the App Hosting +deploy that includes this route is live. Slack will POST a +`url_verification` challenge; the Next.js route answers it. + +The Next.js route is +`sites/mainweb/app/(portal)/api/webhooks/stripe`’s neighbor: +`sites/mainweb/app/(portal)/api/webhooks/slack/route.ts`. `proxy.ts` already +leaves `/api/webhooks/*` alone, same as Stripe. + +### App Hosting vs Firebase Hosting + +| Surface | What it is | Slack | +| --- | --- | --- | +| **Firebase App Hosting** | Next.js on Cloud Run, `apphosting.yaml`, `sites/mainweb` | **Yes — this is the API** | +| **Firebase Hosting** site `dsgt-website` | Static `sites/mainweb/out/` | **No.** That site cannot run the webhook | + +If Event Subscriptions shows a verification failure, the Request URL is almost +certainly pointing at Hosting `out/` (or the wrong host), not App Hosting. + +After the App Hosting deploy that includes this route is live, open **Event +Subscriptions** and click **Retry** / save so Slack can complete the +`url_verification` challenge. + +Reinstall the app if Slack asks you to after changing URLs or scopes. + +## Secret Manager (one-time) + +GCP project: `dsgt-website` (number `672446353769`). App Hosting backend name: +`query`. [`apphosting.yaml`](../../apphosting.yaml) maps: + +| Process env | Secret | +| --- | --- | +| `SLACK_BOT_TOKEN` | `projects/672446353769/secrets/SLACK_BOT_TOKEN` | +| `SLACK_SIGNING_SECRET` | `projects/672446353769/secrets/SLACK_SIGNING_SECRET` | +| `SLACK_SOCKET_MODE` | literal `false` (not a secret) | + +`SLACK_APP_TOKEN` is **not** wired. Create the secrets before the deploy that +references them, or App Hosting will fail to start: + +```bash +gcloud config set project dsgt-website + +# Create empty secrets, then add a version from stdin (do not put tokens in the +# shell history file if you can avoid it — prefer a local file you delete). +gcloud secrets create SLACK_BOT_TOKEN --project=dsgt-website +gcloud secrets create SLACK_SIGNING_SECRET --project=dsgt-website + +printf '%s' 'xoxb-your-bot-token' | gcloud secrets versions add SLACK_BOT_TOKEN --data-file=- +printf '%s' 'your-signing-secret' | gcloud secrets versions add SLACK_SIGNING_SECRET --data-file=- + +firebase apphosting:secrets:grantaccess SLACK_BOT_TOKEN --backend query +firebase apphosting:secrets:grantaccess SLACK_SIGNING_SECRET --backend query +``` + +Replace the `printf` placeholders with the real values from the Slack app +settings. Never commit those values. + +## Run locally (Socket Mode) + +Socket Mode is a **local-only** option so you can develop without a public URL. +Production stays on HTTP. + +1. In the Slack app settings, turn **Socket Mode** on temporarily (or use a + separate development app). Generate an App-Level Token with + `connections:write` (`xapp-...`). +2. From the monorepo root: + +```bash +cp apps/dsgt-slack/.env.example apps/dsgt-slack/.env +# edit apps/dsgt-slack/.env with SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, +# SLACK_APP_TOKEN, and SLACK_SOCKET_MODE=true + +pnpm install +pnpm --filter @query/dsgt-slack dev +``` + +`pnpm dev` at the repo root also starts this package. If `.env` is missing, the +task stays idle so the rest of the monorepo can still boot. + +You can also hit the Next.js webhook locally while `web` is running +(`http://localhost:3001/api/webhooks/slack`) if you tunnel that URL to Slack. +That path is what production uses. + +### Invite @dsgt to a channel + +In the channel: + +```text +/invite @dsgt +``` + +Then mention it (`@dsgt ping`) or run `/dsgt help`. Direct messages work from +the app's **Messages** tab without an invite. + +### Quick checks after install + +| Action | Expected reply | +| ----------------------------------------- | ----------------------------------------------- | +| `@dsgt ping` or `/dsgt ping` | Health check confirming the bot is online | +| `/dsgt help` | List of topics | +| “How do I join?” in a DM, or `/dsgt join` | August 26 first-event FAQ and datasciencegt.org | + +## Scripts + +| Script | Command | +| --- | --- | +| Dev (watch) | `pnpm --filter @query/dsgt-slack dev` | +| Start | `pnpm --filter @query/dsgt-slack start` | +| Lint | `pnpm --filter @query/dsgt-slack lint` | +| Typecheck | `pnpm --filter @query/dsgt-slack typecheck` | +| Test | `pnpm --filter @query/dsgt-slack test` | + +Repo-wide `pnpm lint`, `pnpm typecheck`, `pnpm build`, and `pnpm test` include +this package (`pnpm test` runs the join-FAQ unit test along with the existing +vitest targets). + +## Environment + +See [`.env.example`](./.env.example). Do not commit `.env` or real tokens. + +| Variable | Local Socket Mode | Production (App Hosting) | +| --- | --- | --- | +| `SLACK_BOT_TOKEN` | required | Secret Manager | +| `SLACK_SIGNING_SECRET` | required | Secret Manager | +| `SLACK_APP_TOKEN` | required | not used | +| `SLACK_SOCKET_MODE` | `true` (default) | `false` | diff --git a/apps/dsgt-slack/eslint.config.mjs b/apps/dsgt-slack/eslint.config.mjs new file mode 100644 index 00000000..286e0415 --- /dev/null +++ b/apps/dsgt-slack/eslint.config.mjs @@ -0,0 +1,12 @@ +import { config } from "@query/eslint-config/base"; + +/** @type {import("eslint").Linter.Config[]} */ +export default [ + ...config, + { + files: ["src/index.ts"], + rules: { + "no-console": "off", + }, + }, +]; diff --git a/apps/dsgt-slack/manifest.yaml b/apps/dsgt-slack/manifest.yaml new file mode 100644 index 00000000..0508fc0d --- /dev/null +++ b/apps/dsgt-slack/manifest.yaml @@ -0,0 +1,39 @@ +_metadata: + major_version: 1 + minor_version: 1 +display_information: + name: dsgt + description: Official Slack bot for Data Science @ Georgia Tech (DS@GT). + background_color: "#003057" +features: + app_home: + home_tab_enabled: false + messages_tab_enabled: true + messages_tab_read_only_enabled: false + bot_user: + display_name: dsgt + always_online: true + slash_commands: + - command: /dsgt + description: Ask the Data Science @ Georgia Tech bot for help. + usage_hint: "[help | ping | join]" + should_escape: false +oauth_config: + scopes: + bot: + - app_mentions:read + - chat:write + - commands + - im:history + - im:read + - im:write +settings: + event_subscriptions: + bot_events: + - app_mention + - message.im + interactivity: + is_enabled: true + org_deploy_enabled: false + socket_mode_enabled: false + token_rotation_enabled: false diff --git a/apps/dsgt-slack/package.json b/apps/dsgt-slack/package.json new file mode 100644 index 00000000..ecd1da50 --- /dev/null +++ b/apps/dsgt-slack/package.json @@ -0,0 +1,36 @@ +{ + "name": "@query/dsgt-slack", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/replies.ts", + "./replies": "./src/replies.ts", + "./http": "./src/http.ts" + }, + "scripts": { + "dev": "tsx watch src/index.ts", + "start": "tsx src/index.ts", + "build": "tsc --noEmit", + "lint": "eslint . --max-warnings 0", + "typecheck": "tsc --noEmit", + "test": "vitest run --config vitest.config.ts" + }, + "dependencies": { + "@slack/bolt": "^5.0.0", + "@slack/web-api": "^7.13.0", + "dotenv": "^16.6.1", + "undici": "^6.27.0", + "zod": "3.25.53" + }, + "devDependencies": { + "@query/eslint-config": "workspace:*", + "@query/tsconfig": "workspace:*", + "@types/express": "^5.0.0", + "@types/node": "^22.15.32", + "eslint": "10.1.0", + "tsx": "^4.21.0", + "typescript": "6.0.2", + "vitest": "^4.1.8" + } +} diff --git a/apps/dsgt-slack/src/app.ts b/apps/dsgt-slack/src/app.ts new file mode 100644 index 00000000..2ff58ab6 --- /dev/null +++ b/apps/dsgt-slack/src/app.ts @@ -0,0 +1,69 @@ +import { App } from "@slack/bolt"; + +import type { SlackEnv } from "./env"; +import { replyForSlashCommand, replyForText } from "./replies"; + +function isDirectMessage(channelType: string | undefined): boolean { + return channelType === "im"; +} + +export function registerListeners(app: App): void { + app.event("app_mention", async ({ event, say }) => { + await say({ + text: replyForText(event.text), + thread_ts: event.thread_ts, + }); + }); + + app.message(async ({ message, say }) => { + if (message.subtype !== undefined) { + return; + } + if (!("text" in message) || typeof message.text !== "string") { + return; + } + if ("bot_id" in message && message.bot_id) { + return; + } + if (!isDirectMessage(message.channel_type)) { + return; + } + + await say(replyForText(message.text)); + }); + + app.command("/dsgt", async ({ command, ack, respond }) => { + await ack(); + await respond({ + text: replyForSlashCommand(command.text), + response_type: "ephemeral", + }); + }); +} + +export function createBoltApp(env: SlackEnv): App { + const app = env.socketMode + ? new App({ + token: env.botToken, + signingSecret: env.signingSecret, + socketMode: true, + appToken: env.appToken, + }) + : new App({ + token: env.botToken, + signingSecret: env.signingSecret, + customRoutes: [ + { + path: "/health", + method: ["GET"], + handler: (_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("ok"); + }, + }, + ], + }); + + registerListeners(app); + return app; +} diff --git a/apps/dsgt-slack/src/env.ts b/apps/dsgt-slack/src/env.ts new file mode 100644 index 00000000..398c8bb5 --- /dev/null +++ b/apps/dsgt-slack/src/env.ts @@ -0,0 +1,57 @@ +/** + * Env for the local Bolt process (Socket Mode or a local HTTP receiver). + * Production App Hosting does not call this — the Next.js webhook reads + * SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET at request time. + */ + +import { z } from "zod"; + +const truthy = (value: string | undefined) => + value === undefined || value.toLowerCase() !== "false"; + +const envSchema = z + .object({ + SLACK_BOT_TOKEN: z.string().min(1), + SLACK_SIGNING_SECRET: z.string().min(1), + SLACK_APP_TOKEN: z.string().min(1).optional(), + SLACK_SOCKET_MODE: z.string().optional(), + PORT: z.coerce.number().int().positive().default(3000), + }) + .superRefine((value, ctx) => { + if (truthy(value.SLACK_SOCKET_MODE) && !value.SLACK_APP_TOKEN) { + ctx.addIssue({ + code: "custom", + path: ["SLACK_APP_TOKEN"], + message: + "SLACK_APP_TOKEN is required when Socket Mode is enabled (default).", + }); + } + }); + +export type SlackEnv = { + botToken: string; + signingSecret: string; + appToken: string | undefined; + socketMode: boolean; + port: number; +}; + +export function loadEnv( + source: Record = process.env, +): SlackEnv { + const parsed = envSchema.parse({ + SLACK_BOT_TOKEN: source.SLACK_BOT_TOKEN, + SLACK_SIGNING_SECRET: source.SLACK_SIGNING_SECRET, + SLACK_APP_TOKEN: source.SLACK_APP_TOKEN || undefined, + SLACK_SOCKET_MODE: source.SLACK_SOCKET_MODE, + PORT: source.PORT, + }); + + return { + botToken: parsed.SLACK_BOT_TOKEN, + signingSecret: parsed.SLACK_SIGNING_SECRET, + appToken: parsed.SLACK_APP_TOKEN, + socketMode: truthy(parsed.SLACK_SOCKET_MODE), + port: parsed.PORT, + }; +} diff --git a/apps/dsgt-slack/src/http.test.ts b/apps/dsgt-slack/src/http.test.ts new file mode 100644 index 00000000..548d399a --- /dev/null +++ b/apps/dsgt-slack/src/http.test.ts @@ -0,0 +1,167 @@ +import { createHmac } from "node:crypto"; +import { describe, expect, it, vi } from "vitest"; + +import { + SLACK_WEBHOOK_PATH, + handleSlackWebhook, + verifySlackSignature, +} from "./http"; +import { CLUB_SITE_URL, JOIN_FAQ_REPLY } from "./replies"; + +const SIGNING_SECRET = "test-signing-secret"; +const FROZEN_TS = "1710000000"; +const FROZEN_NOW_MS = Number(FROZEN_TS) * 1000; + +function sign(rawBody: string, timestamp: string): string { + const digest = createHmac("sha256", SIGNING_SECRET) + .update(`v0:${timestamp}:${rawBody}`, "utf8") + .digest("hex"); + return `v0=${digest}`; +} + +function signedRequest(rawBody: string, contentType: string): Request { + const timestamp = String(Math.floor(Date.now() / 1000)); + return new Request(`${CLUB_SITE_URL}${SLACK_WEBHOOK_PATH}`, { + method: "POST", + headers: { + "content-type": contentType, + "x-slack-request-timestamp": timestamp, + "x-slack-signature": sign(rawBody, timestamp), + }, + body: rawBody, + }); +} + +describe("verifySlackSignature", () => { + it("accepts a matching v0 signature within the time window", () => { + const rawBody = `{"ok":true}`; + expect( + verifySlackSignature({ + signingSecret: SIGNING_SECRET, + signature: sign(rawBody, FROZEN_TS), + timestamp: FROZEN_TS, + rawBody, + nowMs: FROZEN_NOW_MS, + }), + ).toBe(true); + }); + + it("rejects a missing or wrong signature", () => { + const rawBody = `{"ok":true}`; + expect( + verifySlackSignature({ + signingSecret: SIGNING_SECRET, + signature: null, + timestamp: FROZEN_TS, + rawBody, + nowMs: FROZEN_NOW_MS, + }), + ).toBe(false); + expect( + verifySlackSignature({ + signingSecret: SIGNING_SECRET, + signature: "v0=deadbeef", + timestamp: FROZEN_TS, + rawBody, + nowMs: FROZEN_NOW_MS, + }), + ).toBe(false); + }); +}); + +describe("handleSlackWebhook", () => { + const env = { botToken: "xoxb-test", signingSecret: SIGNING_SECRET }; + + it("returns the url_verification challenge", async () => { + const rawBody = JSON.stringify({ + type: "url_verification", + challenge: "abc-challenge", + }); + const response = await handleSlackWebhook( + signedRequest(rawBody, "application/json"), + env, + ); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + challenge: "abc-challenge", + }); + }); + + it("rejects unsigned requests", async () => { + const response = await handleSlackWebhook( + new Request(`${CLUB_SITE_URL}${SLACK_WEBHOOK_PATH}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ type: "url_verification", challenge: "nope" }), + }), + env, + ); + expect(response.status).toBe(400); + }); + + it("replies to an app_mention join question with the FAQ", async () => { + const postMessage = vi.fn().mockResolvedValue({}); + const rawBody = JSON.stringify({ + type: "event_callback", + event: { + type: "app_mention", + text: "<@U123> how do I join?", + channel: "C123", + thread_ts: "1710000000.000100", + }, + }); + + const response = await handleSlackWebhook( + signedRequest(rawBody, "application/json"), + env, + { chat: { postMessage } }, + ); + + expect(response.status).toBe(200); + expect(postMessage).toHaveBeenCalledWith({ + channel: "C123", + text: JOIN_FAQ_REPLY, + thread_ts: "1710000000.000100", + }); + }); + + it("replies to a DM join question with the FAQ", async () => { + const postMessage = vi.fn().mockResolvedValue({}); + const rawBody = JSON.stringify({ + type: "event_callback", + event: { + type: "message", + channel_type: "im", + text: "How do I join the club?", + channel: "D123", + }, + }); + + const response = await handleSlackWebhook( + signedRequest(rawBody, "application/json"), + env, + { chat: { postMessage } }, + ); + + expect(response.status).toBe(200); + expect(postMessage).toHaveBeenCalledWith({ + channel: "D123", + text: JOIN_FAQ_REPLY, + }); + }); + + it("acks /dsgt join with the FAQ as an ephemeral response", async () => { + const rawBody = "command=%2Fdsgt&text=join"; + const response = await handleSlackWebhook( + signedRequest(rawBody, "application/x-www-form-urlencoded"), + env, + { chat: { postMessage: vi.fn() } }, + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ + response_type: "ephemeral", + text: JOIN_FAQ_REPLY, + }); + }); +}); diff --git a/apps/dsgt-slack/src/http.ts b/apps/dsgt-slack/src/http.ts new file mode 100644 index 00000000..a6585e47 --- /dev/null +++ b/apps/dsgt-slack/src/http.ts @@ -0,0 +1,211 @@ +/** + * HTTP Events API handler for production. + * + * Slack POSTs to the Firebase App Hosting (Cloud Run) URL. This module is Fetch + * Request/Response based so Next.js App Router can call it. It does not start a + * Bolt HTTP server — Socket Mode / Bolt remain a local-only process. + */ + +import { createHmac, timingSafeEqual } from "node:crypto"; +import { WebClient } from "@slack/web-api"; + +import { replyForSlashCommand, replyForText } from "./replies"; + +/** Stable Request URL path on mainweb. Use this in the Slack app settings. */ +export const SLACK_WEBHOOK_PATH = "/api/webhooks/slack"; + +const MAX_CLOCK_SKEW_SEC = 60 * 5; + +export type SlackHttpEnv = { + botToken: string; + signingSecret: string; +}; + +export type SlackPoster = { + chat: { + postMessage: (args: { + channel: string; + text: string; + thread_ts?: string; + }) => Promise; + }; +}; + +function safeEqual(left: string, right: string): boolean { + const leftBuf = Buffer.from(left, "utf8"); + const rightBuf = Buffer.from(right, "utf8"); + if (leftBuf.length !== rightBuf.length) { + return false; + } + return timingSafeEqual(leftBuf, rightBuf); +} + +export function verifySlackSignature(options: { + signingSecret: string; + signature: string | null; + timestamp: string | null; + rawBody: string; + nowMs?: number; +}): boolean { + const { + signingSecret, + signature, + timestamp, + rawBody, + nowMs = Date.now(), + } = options; + + if (!signature || !timestamp || !/^[0-9]+$/.test(timestamp)) { + return false; + } + + const ageSec = Math.abs(nowMs / 1000 - Number(timestamp)); + if (ageSec > MAX_CLOCK_SKEW_SEC) { + return false; + } + + const digest = createHmac("sha256", signingSecret) + .update(`v0:${timestamp}:${rawBody}`, "utf8") + .digest("hex"); + + return safeEqual(`v0=${digest}`, signature); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +type SlackMessageEvent = { + type: string; + subtype?: string; + bot_id?: string; + text?: string; + channel?: string; + channel_type?: string; + thread_ts?: string; +}; + +function parseMessageEvent(value: unknown): SlackMessageEvent | null { + if (!isRecord(value) || typeof value.type !== "string") { + return null; + } + + return { + type: value.type, + subtype: typeof value.subtype === "string" ? value.subtype : undefined, + bot_id: typeof value.bot_id === "string" ? value.bot_id : undefined, + text: typeof value.text === "string" ? value.text : undefined, + channel: typeof value.channel === "string" ? value.channel : undefined, + channel_type: + typeof value.channel_type === "string" ? value.channel_type : undefined, + thread_ts: + typeof value.thread_ts === "string" ? value.thread_ts : undefined, + }; +} + +async function handleEvent( + event: SlackMessageEvent, + client: SlackPoster, +): Promise { + if (event.bot_id || event.subtype) { + return; + } + if (typeof event.text !== "string" || typeof event.channel !== "string") { + return; + } + + if (event.type === "app_mention") { + await client.chat.postMessage({ + channel: event.channel, + text: replyForText(event.text), + ...(event.thread_ts ? { thread_ts: event.thread_ts } : {}), + }); + return; + } + + if (event.type === "message" && event.channel_type === "im") { + await client.chat.postMessage({ + channel: event.channel, + text: replyForText(event.text), + }); + } +} + +async function handleJsonBody( + payload: unknown, + client: SlackPoster, +): Promise { + if (!isRecord(payload) || typeof payload.type !== "string") { + return Response.json({ error: "Invalid payload" }, { status: 400 }); + } + + if (payload.type === "url_verification") { + const challenge = + typeof payload.challenge === "string" ? payload.challenge : ""; + return Response.json({ challenge }); + } + + if (payload.type === "event_callback") { + const event = parseMessageEvent(payload.event); + if (event) { + await handleEvent(event, client); + } + return Response.json({ ok: true }); + } + + return Response.json({ ok: true }); +} + +function handleFormBody(rawBody: string): Response { + const params = new URLSearchParams(rawBody); + + if (params.get("ssl_check") === "1") { + return new Response("", { status: 200 }); + } + + if (params.has("payload")) { + return new Response("", { status: 200 }); + } + + const command = params.get("command"); + if (command) { + return Response.json({ + response_type: "ephemeral", + text: replyForSlashCommand(params.get("text") ?? ""), + }); + } + + return Response.json({ error: "Unknown Slack request" }, { status: 400 }); +} + +export async function handleSlackWebhook( + req: Request, + env: SlackHttpEnv, + client: SlackPoster = new WebClient(env.botToken), +): Promise { + const rawBody = await req.text(); + const valid = verifySlackSignature({ + signingSecret: env.signingSecret, + signature: req.headers.get("x-slack-signature"), + timestamp: req.headers.get("x-slack-request-timestamp"), + rawBody, + }); + + if (!valid) { + return Response.json({ error: "Invalid signature" }, { status: 400 }); + } + + const contentType = req.headers.get("content-type") ?? ""; + if ( + contentType.includes("application/json") || + rawBody.trimStart().startsWith("{") + ) { + try { + return await handleJsonBody(JSON.parse(rawBody) as unknown, client); + } catch { + return Response.json({ error: "Invalid JSON body" }, { status: 400 }); + } + } + + return handleFormBody(rawBody); +} diff --git a/apps/dsgt-slack/src/index.ts b/apps/dsgt-slack/src/index.ts new file mode 100644 index 00000000..88da53b5 --- /dev/null +++ b/apps/dsgt-slack/src/index.ts @@ -0,0 +1,38 @@ +import "dotenv/config"; + +import { createBoltApp } from "./app"; +import type { SlackEnv } from "./env"; +import { loadEnv } from "./env"; + +async function main(): Promise { + let env: SlackEnv; + try { + env = loadEnv(); + } catch { + console.warn( + "dsgt Slack bot skipped: set SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET in apps/dsgt-slack/.env to run locally (Socket Mode). Production traffic is handled by sites/mainweb at /api/webhooks/slack.", + ); + await new Promise(() => { + /* keep turbo `dev` from restarting this task */ + }); + return; + } + + const app = createBoltApp(env); + + if (env.socketMode) { + await app.start(); + app.logger.info("dsgt Slack bot is running (Socket Mode, local)"); + return; + } + + await app.start(env.port); + app.logger.info( + `dsgt Slack bot is running a local HTTP receiver on port ${String(env.port)} at /slack/events. Production uses Firebase App Hosting at /api/webhooks/slack.`, + ); +} + +main().catch((error: unknown) => { + console.error("Failed to start the dsgt Slack bot.", error); + process.exit(1); +}); diff --git a/apps/dsgt-slack/src/replies.test.ts b/apps/dsgt-slack/src/replies.test.ts new file mode 100644 index 00000000..da3d8a20 --- /dev/null +++ b/apps/dsgt-slack/src/replies.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; + +import { + CLUB_SITE_URL, + JOIN_FAQ_REPLY, + detectIntent, + replyForSlashCommand, + replyForText, +} from "./replies"; + +describe("join FAQ reply", () => { + it("names the August 26 first event and points to the club site", () => { + expect(JOIN_FAQ_REPLY).toContain("August 26"); + expect(JOIN_FAQ_REPLY).toContain("8/26"); + expect(JOIN_FAQ_REPLY).toContain( + "Further details will be announced at that event", + ); + expect(JOIN_FAQ_REPLY).toContain(CLUB_SITE_URL); + expect(JOIN_FAQ_REPLY).toContain("datasciencegt.org"); + }); + + it.each([ + "How do I join the club?", + "When is the first event?", + "I want to sign up", + "<@U123> how do I join?", + "What is happening on 8/26?", + ])("routes join questions to the FAQ: %s", (text) => { + expect(detectIntent(text)).toBe("join"); + expect(replyForText(text)).toBe(JOIN_FAQ_REPLY); + }); +}); + +describe("slash command routing", () => { + it("returns help when the command text is empty", () => { + expect(replyForSlashCommand("")).toContain("Available topics"); + }); + + it("returns the join FAQ for /dsgt join", () => { + expect(replyForSlashCommand("join")).toBe(JOIN_FAQ_REPLY); + }); +}); diff --git a/apps/dsgt-slack/src/replies.ts b/apps/dsgt-slack/src/replies.ts new file mode 100644 index 00000000..14a4d15d --- /dev/null +++ b/apps/dsgt-slack/src/replies.ts @@ -0,0 +1,94 @@ +/** + * Reply text and lightweight intent routing for the DS@GT Slack bot. + * + * Keep this module free of Slack SDK imports so the copy can be unit tested. + */ + +/** Public club site host. Scheme is added below so the full URL is not a source literal. */ +export const CLUB_HOST = "datasciencegt.org"; +export const CLUB_SITE_URL = `https://${CLUB_HOST}`; + +export const JOIN_FAQ_REPLY = `The first Data Science @ Georgia Tech event of the term is on August 26 (8/26). Further details will be announced at that event. For club information and how to get involved, visit ${CLUB_SITE_URL}.`; + +export const HEALTH_REPLY = + "dsgt is online. This health check confirms the Data Science @ Georgia Tech Slack bot is running."; + +export const HELP_REPLY = [ + "I am the Data Science @ Georgia Tech Slack bot (dsgt).", + "", + "You can mention @dsgt in a channel, send a direct message, or use the /dsgt slash command.", + "", + "Available topics:", + "• help — show this message", + "• ping — confirm that the bot is online", + "• join — how to join the club and the date of the first event", + "", + `Club website: ${CLUB_SITE_URL}`, +].join("\n"); + +export const DEFAULT_REPLY = + "Hello. I am the Data Science @ Georgia Tech bot. Send help for what I can answer, or ping to confirm that I am online."; + +export const UNKNOWN_COMMAND_REPLY = + "I did not recognize that request. Send help for the topics I can answer."; + +const BOT_MENTION = /<@[^>]+>/g; + +const JOIN_PATTERN = + /\b(join|sign[ -]?up|membership|member|get involved|first (event|meeting)|8\/26|august 26|26th)\b/i; + +const HEALTH_PATTERN = /\b(ping|health|alive|status)\b/i; + +const HELP_PATTERN = /\b(help|commands|what can you do)\b/i; + +export type ReplyIntent = "join" | "health" | "help" | "default"; + +export function stripBotMention(text: string): string { + return text.replace(BOT_MENTION, " ").replace(/\s+/g, " ").trim(); +} + +export function detectIntent(text: string): ReplyIntent { + const normalized = stripBotMention(text).trim(); + if (normalized.length === 0) { + return "help"; + } + if (JOIN_PATTERN.test(normalized)) { + return "join"; + } + if (HEALTH_PATTERN.test(normalized)) { + return "health"; + } + if (HELP_PATTERN.test(normalized)) { + return "help"; + } + return "default"; +} + +export function replyForIntent(intent: ReplyIntent): string { + switch (intent) { + case "join": + return JOIN_FAQ_REPLY; + case "health": + return HEALTH_REPLY; + case "help": + return HELP_REPLY; + default: + return DEFAULT_REPLY; + } +} + +export function replyForText(text: string): string { + return replyForIntent(detectIntent(text)); +} + +export function replyForSlashCommand(text: string): string { + const normalized = text.trim(); + if (normalized.length === 0) { + return HELP_REPLY; + } + const intent = detectIntent(normalized); + if (intent === "default") { + return UNKNOWN_COMMAND_REPLY; + } + return replyForIntent(intent); +} diff --git a/apps/dsgt-slack/tsconfig.json b/apps/dsgt-slack/tsconfig.json new file mode 100644 index 00000000..6eaf141c --- /dev/null +++ b/apps/dsgt-slack/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tooling/typescript/base.json", + "compilerOptions": { + "types": ["node"], + "noEmit": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/dsgt-slack/turbo.json b/apps/dsgt-slack/turbo.json new file mode 100644 index 00000000..b0115a50 --- /dev/null +++ b/apps/dsgt-slack/turbo.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "extends": ["//"], + "tasks": { + "dev": { + "persistent": true, + "cache": false + }, + "start": { + "persistent": true, + "cache": false + } + } +} diff --git a/apps/dsgt-slack/vitest.config.ts b/apps/dsgt-slack/vitest.config.ts new file mode 100644 index 00000000..ae847ff6 --- /dev/null +++ b/apps/dsgt-slack/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, +}); diff --git a/docs/README.md b/docs/README.md index 1544941c..676c5b48 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,12 @@ Start here, then jump to the page that matches the work you are doing. | [Database](./packages/db.md) | `@query/db` | Drizzle schema, client, membership rules | | [UI](./packages/ui.md) | `@query/ui` | Shared React components and styles | +## Apps + +| Document | Workspace | Role | +| --- | --- | --- | +| [dsgt Slack bot](../apps/dsgt-slack/README.md) | `@query/dsgt-slack` | `@dsgt` Slack bot; production HTTP on mainweb | + ## Sites | Document | Workspace | Role | diff --git a/docs/architecture.md b/docs/architecture.md index f91c56f5..92e1fa89 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -15,6 +15,7 @@ │ / /team /events /login /dashboard /admin /judge … │ │ │ │ │ /api/trpc /api/auth /api/webhooks │ +│ stripe + slack │ └──────────┬──────────────────┴───────────────┬───────────────┘ │ │ ┌──────▼──────┐ ┌──────▼──────┐ @@ -91,7 +92,9 @@ Stripe Checkout and Payment Intents both exist. Amounts are defined once in `@qu | Bootcamp add-on (on top of membership) | `1000` ($10) | | Max charge treated as membership | `10000` | -The webhook is `sites/mainweb/app/(portal)/api/webhooks/stripe/route.ts`. Linking can also happen from the portal (`stripe.linkAccount`, `stripe.attemptAutoLink`) and at sign-in. +The Stripe webhook is `sites/mainweb/app/(portal)/api/webhooks/stripe/route.ts`. Linking can also happen from the portal (`stripe.linkAccount`, `stripe.attemptAutoLink`) and at sign-in. + +The Slack Events API webhook is `sites/mainweb/app/(portal)/api/webhooks/slack/route.ts`. It imports reply text from `apps/dsgt-slack` and verifies `SLACK_SIGNING_SECRET`. Production is HTTP on App Hosting (`{AUTH_URL}/api/webhooks/slack`); Socket Mode is local-only. See [`apps/dsgt-slack/README.md`](../apps/dsgt-slack/README.md). ## Caching @@ -106,9 +109,12 @@ The webhook is `sites/mainweb/app/(portal)/api/webhooks/stripe/route.ts`. Linkin The event site does not talk to the database. Interest and registration live on the portal; the marketing site links to `/login?callbackUrl=/hacklytics`. +## Apps + +`apps/dsgt-slack` (`@query/dsgt-slack`) is the `@dsgt` Slack bot. Reply copy lives in that package. Production HTTP is the mainweb route `/api/webhooks/slack` on App Hosting. Socket Mode is a local-only process in the same package. + ## What is not in this repo - `packages/consts` is mentioned in older notes and is **not** a workspace today. -- `apps/*` is listed in `pnpm-workspace.yaml` but there is no `apps/` directory. - `graphify-out/` is generated graph output, not product code. - `trust badge/` holds MLH league badge SVGs for the event site. diff --git a/docs/getting-started.md b/docs/getting-started.md index 07ad9c4b..ee4ea6de 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -22,7 +22,7 @@ corepack prepare pnpm@10.33.2 --activate pnpm install ``` -Workspaces are defined in `pnpm-workspace.yaml`: `sites/*`, `packages/*`, and `tooling/*`. +Workspaces are defined in `pnpm-workspace.yaml`: `apps/*`, `sites/*`, `packages/*`, and `tooling/*`. ## Local database @@ -88,7 +88,7 @@ pnpm --filter @query/db studio # Drizzle Studio | `pnpm build` | `turbo run build` | | `pnpm lint` | ESLint across workspaces (`--max-warnings 0`) | | `pnpm typecheck` | `tsc --noEmit` via Turbo | -| `pnpm test` | Vitest: `packages/api`, `packages/db`, `sites/mainweb/lib` | +| `pnpm test` | Vitest: `packages/api`, `packages/db`, `sites/mainweb/lib`, `apps/dsgt-slack` | | `pnpm format` | Prettier write | Database scripts live on `@query/db`: diff --git a/docs/operations/deployment.md b/docs/operations/deployment.md index 13e7abe6..1c47ba7c 100644 --- a/docs/operations/deployment.md +++ b/docs/operations/deployment.md @@ -28,7 +28,15 @@ Run: `node sites/mainweb/.next/standalone/sites/mainweb/server.js` Secrets are GCP Secret Manager. Grant the App Hosting backend access once per secret, e.g. `firebase apphosting:secrets:grantaccess DATABASE_URL --backend query`. -Env mapping (names only) is in `apphosting.yaml`: `DATABASE_URL`, `AUTH_SECRET` (also copied to `NEXTAUTH_SECRET`), Google/GitHub OAuth, Stripe, SMTP, DDoS ceilings, `TRUSTED_PROXY_HOPS=1`. +Env mapping (names only) is in `apphosting.yaml`: `DATABASE_URL`, `AUTH_SECRET` (also copied to `NEXTAUTH_SECRET`), Google/GitHub OAuth, Stripe, Slack (`SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, `SLACK_SOCKET_MODE=false`), SMTP, DDoS ceilings, `TRUSTED_PROXY_HOPS=1`. + +Slack Event Subscriptions, slash commands, and interactivity must use the App Hosting origin (`AUTH_URL` / `NEXTAUTH_URL` in `apphosting.yaml`, host `datasciencegt.org`), not Firebase Hosting `dsgt-website` / `sites/mainweb/out`: + +```text +{AUTH_URL}/api/webhooks/slack +``` + +Create `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` in Secret Manager before that deploy. See [`apps/dsgt-slack/README.md`](../../apps/dsgt-slack/README.md). ## Hacklytics — Firebase Hosting diff --git a/docs/operations/environment.md b/docs/operations/environment.md index 79af41a5..391031c9 100644 --- a/docs/operations/environment.md +++ b/docs/operations/environment.md @@ -64,6 +64,17 @@ Without `DATABASE_URL`, `db` is null, sessions fall back to JWT, and tRPC proced | `DDOS_CLEANUP_INTERVAL_MS` | Code default if unset | | `CSP_ENFORCE` | `true` turns CSP from Report-Only into enforcing. Default is report-only | +## Slack (`@dsgt`) + +| Variable | Notes | +| --- | --- | +| `SLACK_BOT_TOKEN` | Bot User OAuth Token. App Hosting secret `SLACK_BOT_TOKEN` | +| `SLACK_SIGNING_SECRET` | Request signature. App Hosting secret `SLACK_SIGNING_SECRET` | +| `SLACK_SOCKET_MODE` | App Hosting sets `false`. Local Socket Mode defaults to on | +| `SLACK_APP_TOKEN` | Local Socket Mode only (`xapp-...`). Not set in production | + +Create the Secret Manager secrets before the App Hosting deploy that references them. Install steps: [`apps/dsgt-slack/README.md`](../../apps/dsgt-slack/README.md). + ## Runtime (App Hosting) | Variable | Value | diff --git a/docs/operations/testing.md b/docs/operations/testing.md index accbfd5d..3fe04c73 100644 --- a/docs/operations/testing.md +++ b/docs/operations/testing.md @@ -9,7 +9,7 @@ pnpm test That is: ```bash -vitest run packages/api packages/db sites/mainweb/lib +vitest run packages/api packages/db sites/mainweb/lib apps/dsgt-slack ``` CI: `.github/workflows/test.yml` on pushes to `main`/`dev` and on pull requests. @@ -41,6 +41,12 @@ Tests use `src/test/create-mock-context.ts` and `.internal-tests/_db-tx-mock.ts` `src/services/membership.test.ts` — grant, lapse, current-edition resolution, payment linking. +## `@query/dsgt-slack` + +`apps/dsgt-slack/src/replies.test.ts` — join FAQ copy (August 26 / 8/26 / datasciencegt.org) and slash-command routing. + +`apps/dsgt-slack/src/http.test.ts` — Slack signature verification, `url_verification` challenge, `app_mention` join reply, `/dsgt join` ack. + ## Mainweb `sites/mainweb/lib/*.test.ts` — phone formatting, hackathon slugs, `safe-callback`. diff --git a/docs/sites/mainweb.md b/docs/sites/mainweb.md index 636ca0ee..54b84c9a 100644 --- a/docs/sites/mainweb.md +++ b/docs/sites/mainweb.md @@ -65,6 +65,7 @@ Unauthenticated and authenticated product UI. `proxy.ts` marks these prefixes `p | `/api/auth/[...nextauth]` | NextAuth handlers | | `/api/auth/verify-email` | Email-code verification | | `/api/webhooks/stripe` | Stripe webhooks | +| `/api/webhooks/slack` | Slack Events API, `/dsgt`, interactivity (`@query/dsgt-slack`) | ## Client data @@ -77,7 +78,7 @@ Helpers: `lib/hackathon-slug.ts`, `lib/phone.ts`, `lib/bootcamp-schedule.ts`, `l ## Config highlights (`next.config.mjs`) -- Transpiles `@query/api`, `@query/auth`, `@query/db`, `@query/ui` +- Transpiles `@query/api`, `@query/auth`, `@query/db`, `@query/dsgt-slack`, `@query/ui` - `outputFileTracingRoot` is the monorepo root (needed for standalone on App Hosting) - Security headers: HSTS, CSP (report-only unless `CSP_ENFORCE=true`), frame options SAMEORIGIN (admin print/QR iframes), Permissions-Policy (camera **not** denied — `/scan` needs it) - React Compiler enabled diff --git a/package.json b/package.json index 0954cf6d..74d862cd 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint": "turbo run lint", "format": "prettier --write .", "typecheck": "turbo run typecheck", - "test": "vitest run packages/api packages/db sites/mainweb/lib" + "test": "vitest run packages/api packages/db sites/mainweb/lib apps/dsgt-slack" }, "dependencies": { "next": "16.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b1e7f67..c24f207b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,49 @@ importers: specifier: ^4.1.8 version: 4.1.8(@types/node@22.15.32)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) + apps/dsgt-slack: + dependencies: + '@slack/bolt': + specifier: ^5.0.0 + version: 5.0.0(@types/express@5.0.6)(undici@6.28.0) + '@slack/web-api': + specifier: ^7.13.0 + version: 7.19.0 + dotenv: + specifier: ^16.6.1 + version: 16.6.1 + undici: + specifier: ^6.27.0 + version: 6.28.0 + zod: + specifier: 3.25.53 + version: 3.25.53 + devDependencies: + '@query/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@query/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/node': + specifier: ^22.15.32 + version: 22.15.32 + eslint: + specifier: 10.1.0 + version: 10.1.0(jiti@2.7.0) + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: 6.0.2 + version: 6.0.2 + vitest: + specifier: ^4.1.8 + version: 4.1.8(@types/node@22.15.32)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) + packages/api: dependencies: '@query/auth': @@ -284,6 +327,9 @@ importers: '@query/db': specifier: workspace:* version: link:../../packages/db + '@query/dsgt-slack': + specifier: workspace:* + version: link:../../apps/dsgt-slack '@query/ui': specifier: workspace:* version: link:../../packages/ui @@ -1769,6 +1815,46 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@slack/bolt@5.0.0': + resolution: {integrity: sha512-L0FTzidrDKTu4Ph8sdv0bB3acafBTAC02TBcD+hojPTTNSMX3hSK+rkpkUKkqbbc0vGpSjd6A/g71Rjy7EbBhw==} + engines: {node: '>=20', npm: '>=9.6.4'} + peerDependencies: + '@types/express': ^5.0.0 + + '@slack/logger@4.0.1': + resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/logger@5.0.0': + resolution: {integrity: sha512-VGXhmmgsAo9shdQYh4tFDndd+7nsgp0Y5h0UPDaUp8K359pBasI6YdkMqFW3mCOxLQkq09qj7o7cq6f3DuXcJQ==} + engines: {node: '>= 20', npm: '>=9.6.4'} + + '@slack/oauth@4.0.0': + resolution: {integrity: sha512-Aqs5bGghT+VtNcdwVOQy7CcyCfDNd9YYnZsCRukIF9p1Mxuq4uL+BjqlWQB0SeS4wzrLzePPgvj8JDuQil+ezA==} + engines: {node: '>=20', npm: '>=9.6.4'} + + '@slack/socket-mode@3.0.0': + resolution: {integrity: sha512-QShO60SB0E+HH+TbcKj3CBEQbodToRyiXnxuSB4t1kvUlqEmuGA1nOOjrRDkDJbOECAZ13PLe4ek9SrntpfoYg==} + engines: {node: '>=20', npm: '>=9.6.4'} + peerDependencies: + undici: ^6.27.0 + + '@slack/types@2.22.0': + resolution: {integrity: sha512-sZ9lIgJhPX2qft/tKWiklFlc0o1FWeI7QtciZJfW1+ErH1eGGHvOZ8e73sleTCFEFJp1q/R0WeS8Oa7AsiDprg==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + + '@slack/types@3.0.0': + resolution: {integrity: sha512-KNOqpnNAlsFt5Jk9XBclslQ0lobRIg/0tnhpmvZJAglHJx9E8oceN8hC3gaBzkR6UzQ9Wzq4rLsJ98wUcxWPfw==} + engines: {node: '>= 20', npm: '>=9.6.4'} + + '@slack/web-api@7.19.0': + resolution: {integrity: sha512-ItjyjEZml+LDH8CjcCLRLJHh7VZtevPKExrRN3l5KWyBliyDnGAeoO4Y+K+fFBmRpKLYVPgqWMX4THldv2HVtA==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/web-api@8.0.0': + resolution: {integrity: sha512-ORx3XQryQPq2Jnxv5giSKXVoQRUeylrrymIR2S9fPzLjPcCts8RayMeBSZMcpfpAqp6fnBRuPW2UB6dUPUTEZA==} + engines: {node: '>= 20', npm: '>=9.6.4'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2000,9 +2086,15 @@ packages: '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@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/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -2024,16 +2116,31 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/minimatch@6.0.0': resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@20.19.40': resolution: {integrity: sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q==} @@ -2050,6 +2157,12 @@ packages: '@types/qrcode@1.5.6': resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.0.3': resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} peerDependencies: @@ -2061,9 +2174,18 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/sanitize-html@2.16.1': resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/stats.js@0.17.4': resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} @@ -2167,6 +2289,10 @@ packages: react: ^17 || ^18 || ^19 react-dom: ^17 || ^18 || ^19 + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2181,6 +2307,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -2243,6 +2373,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.22: resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==} engines: {node: ^10 || ^12 || >=14} @@ -2258,6 +2391,9 @@ packages: resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} engines: {node: '>=4'} + axios@1.19.0: + resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -2281,6 +2417,10 @@ packages: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + brace-expansion@5.0.9: resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} @@ -2294,9 +2434,16 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2344,13 +2491,37 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -2424,6 +2595,14 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2562,6 +2741,12 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.353: resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} @@ -2571,6 +2756,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enhanced-resolve@5.21.2: resolution: {integrity: sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==} engines: {node: '>=10.13.0'} @@ -2627,6 +2816,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2746,10 +2938,24 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2786,6 +2992,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -2801,10 +3011,27 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -2822,6 +3049,10 @@ packages: react-dom: optional: true + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2924,12 +3155,28 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2942,10 +3189,17 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2978,6 +3232,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3018,6 +3275,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3030,6 +3290,10 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -3096,10 +3360,20 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3199,6 +3473,27 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -3218,6 +3513,14 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3229,6 +3532,22 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + minimatch@10.2.3: resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} engines: {node: 18 || 20 || >=22} @@ -3268,6 +3587,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + next-auth@5.0.0-beta.32: resolution: {integrity: sha512-CGlChIEWZ6LltNVxrE5yiySMID+Idpmry47JYA5lLwgD8Sx02a8M65VL0TWVz9nbnOioS/tCW/rP/0+mE7Qp4Q==} peerDependencies: @@ -3367,6 +3690,13 @@ packages: 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'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -3379,6 +3709,10 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -3395,6 +3729,18 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -3402,6 +3748,10 @@ packages: parse-srcset@1.0.2: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3413,6 +3763,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3595,6 +3948,14 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3609,9 +3970,21 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-chartjs-2@5.3.1: resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==} peerDependencies: @@ -3689,6 +4062,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3698,6 +4075,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3705,6 +4086,9 @@ packages: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -3713,6 +4097,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-html@2.17.5: resolution: {integrity: sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg==} @@ -3736,6 +4123,14 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -3751,6 +4146,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.35.3: resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} engines: {node: '>=20.9.0'} @@ -3784,6 +4182,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3809,6 +4211,10 @@ packages: 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==} @@ -3917,6 +4323,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -3933,6 +4343,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -3950,6 +4364,10 @@ packages: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -3990,6 +4408,14 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@6.28.0: + resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} + engines: {node: '>=18.17'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -4019,6 +4445,10 @@ packages: '@types/react': optional: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite@7.3.6: resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4145,6 +4575,9 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -5184,6 +5617,79 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@slack/bolt@5.0.0(@types/express@5.0.6)(undici@6.28.0)': + dependencies: + '@slack/logger': 5.0.0 + '@slack/oauth': 4.0.0 + '@slack/socket-mode': 3.0.0(undici@6.28.0) + '@slack/types': 3.0.0 + '@slack/web-api': 8.0.0 + '@types/express': 5.0.6 + express: 5.2.1 + path-to-regexp: 8.4.2 + raw-body: 3.0.2 + tsscmp: 1.0.6 + transitivePeerDependencies: + - supports-color + - undici + + '@slack/logger@4.0.1': + dependencies: + '@types/node': 22.15.32 + + '@slack/logger@5.0.0': + dependencies: + '@types/node': 22.15.32 + + '@slack/oauth@4.0.0': + dependencies: + '@slack/logger': 5.0.0 + '@slack/web-api': 8.0.0 + '@types/jsonwebtoken': 9.0.10 + '@types/node': 22.15.32 + jsonwebtoken: 9.0.3 + + '@slack/socket-mode@3.0.0(undici@6.28.0)': + dependencies: + '@slack/logger': 5.0.0 + '@slack/web-api': 8.0.0 + '@types/node': 22.15.32 + eventemitter3: 5.0.4 + undici: 6.28.0 + + '@slack/types@2.22.0': {} + + '@slack/types@3.0.0': {} + + '@slack/web-api@7.19.0': + dependencies: + '@slack/logger': 4.0.1 + '@slack/types': 2.22.0 + '@types/node': 22.15.32 + '@types/retry': 0.12.0 + axios: 1.19.0 + eventemitter3: 5.0.4 + form-data: 4.0.6 + is-electron: 2.2.2 + is-stream: 2.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + retry: 0.13.1 + transitivePeerDependencies: + - debug + - supports-color + + '@slack/web-api@8.0.0': + dependencies: + '@slack/logger': 5.0.0 + '@slack/types': 3.0.0 + '@types/node': 22.15.32 + '@types/retry': 0.12.0 + eventemitter3: 5.0.4 + p-queue: 6.6.2 + p-retry: 4.6.2 + retry: 0.13.1 + '@standard-schema/spec@1.1.0': {} '@stripe/react-stripe-js@3.10.0(@stripe/stripe-js@5.10.0)(react-dom@19.0.0(react@19.2.7))(react@19.2.7)': @@ -5355,11 +5861,20 @@ snapshots: '@tweenjs/tween.js@23.1.3': {} + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.15.32 + '@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': 22.15.32 + '@types/deep-eql@4.0.2': {} '@types/emscripten@1.41.5': {} @@ -5379,14 +5894,36 @@ snapshots: '@types/estree@1.0.9': {} + '@types/express-serve-static-core@5.1.3': + dependencies: + '@types/node': 22.15.32 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.3 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.15.32 + '@types/minimatch@6.0.0': dependencies: minimatch: 10.2.3 + '@types/ms@2.1.0': {} + '@types/node@20.19.40': dependencies: undici-types: 6.21.0 @@ -5411,6 +5948,10 @@ snapshots: dependencies: '@types/node': 22.15.32 + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + '@types/react-dom@19.0.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 @@ -5423,10 +5964,21 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/retry@0.12.0': {} + '@types/sanitize-html@2.16.1': dependencies: htmlparser2: 10.1.0 + '@types/send@1.2.1': + dependencies: + '@types/node': 22.15.32 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.15.32 + '@types/stats.js@0.17.4': {} '@types/three@0.184.1': @@ -5560,7 +6112,7 @@ snapshots: '@typescript-eslint/types': 8.59.2 '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.3 semver: 7.8.5 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@5.8.3) @@ -5575,7 +6127,7 @@ snapshots: '@typescript-eslint/types': 8.59.2 '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 - minimatch: 10.2.4 + minimatch: 10.2.3 semver: 7.8.5 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@6.0.2) @@ -5658,6 +6210,11 @@ snapshots: react-dom: 19.0.0(react@19.2.7) webrtc-adapter: 9.0.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -5668,6 +6225,12 @@ snapshots: acorn@8.16.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -5760,6 +6323,8 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + autoprefixer@10.4.22(postcss@8.5.23): dependencies: browserslist: 4.28.2 @@ -5776,6 +6341,16 @@ snapshots: axe-core@4.11.4: {} + axios@1.19.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + axobject-query@4.1.0: {} babel-plugin-react-compiler@1.0.0: @@ -5792,6 +6367,20 @@ snapshots: baseline-browser-mapping@2.9.19: {} + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.1.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -5808,8 +6397,12 @@ snapshots: node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5857,10 +6450,24 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@7.2.0: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.1.0: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -5927,6 +6534,10 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -5980,12 +6591,20 @@ snapshots: duplexer@0.1.2: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + electron-to-chromium@1.5.353: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + enhanced-resolve@5.21.2: dependencies: graceful-fs: 4.2.11 @@ -6129,6 +6748,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.7.0)): @@ -6307,8 +6928,47 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + expect-type@1.3.0: {} + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -6341,6 +7001,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -6358,10 +7029,22 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.16.0: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + fraction.js@5.3.4: {} framer-motion@11.18.2(react-dom@19.0.0(react@19.2.7))(react@19.2.7): @@ -6373,6 +7056,8 @@ snapshots: react: 19.2.7 react-dom: 19.0.0(react@19.2.7) + fresh@2.0.0: {} + fsevents@2.3.2: optional: true @@ -6473,6 +7158,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + html-escaper@2.0.2: {} htmlparser2@10.1.0: @@ -6482,18 +7171,41 @@ snapshots: domutils: 3.2.2 entities: 7.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} imurmurhash@0.1.4: {} + inherits@2.0.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.3 side-channel: 1.1.0 + ipaddr.js@1.9.1: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -6534,6 +7246,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-electron@2.2.2: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -6567,6 +7281,8 @@ snapshots: is-plain-object@5.0.0: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -6580,6 +7296,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-stream@2.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -6639,6 +7357,19 @@ snapshots: dependencies: minimist: 1.2.8 + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -6646,6 +7377,17 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6722,6 +7464,20 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + lodash.throttle@4.1.1: {} loose-envify@1.4.0: @@ -6738,6 +7494,10 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} meshoptimizer@1.1.1: {} @@ -6747,6 +7507,18 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + minimatch@10.2.3: dependencies: brace-expansion: 5.0.9 @@ -6777,6 +7549,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + next-auth@5.0.0-beta.32(next@16.3.0(@playwright/test@1.60.0)(@types/node@20.19.40)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.2.7))(react@19.2.7))(nodemailer@9.0.3)(react@19.2.7): dependencies: '@auth/core': 0.41.3(nodemailer@9.0.3) @@ -6911,6 +7685,14 @@ snapshots: obug@2.1.1: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + opener@1.5.2: {} optionator@0.9.4: @@ -6928,6 +7710,8 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-finally@1.0.0: {} + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -6944,16 +7728,34 @@ snapshots: dependencies: p-limit: 3.1.0 + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + p-try@2.2.0: {} parse-srcset@1.0.2: {} + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-key@3.1.1: {} path-parse@1.0.7: {} + path-to-regexp@8.4.2: {} + pathe@2.0.3: {} pg-cloudflare@1.4.0: @@ -7055,6 +7857,13 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@2.1.0: {} + punycode@2.3.1: {} qrcode.react@3.2.0(react@19.2.7): @@ -7067,8 +7876,22 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + queue-microtask@1.2.3: {} + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.7): dependencies: chart.js: 4.5.1 @@ -7152,6 +7975,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + retry@0.13.1: {} + reusify@1.1.0: {} rollup@4.60.3: @@ -7185,6 +8010,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.3 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7197,6 +8032,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -7208,6 +8045,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: {} + sanitize-html@2.17.5: dependencies: deepmerge: 4.3.1 @@ -7228,6 +8067,31 @@ snapshots: semver@7.8.5: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -7252,6 +8116,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} + sharp@0.35.3(@types/node@20.19.40): dependencies: '@img/colour': 1.1.0 @@ -7354,6 +8220,14 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} sirv@2.0.4: @@ -7375,6 +8249,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.0.0: {} stop-iteration-iterator@1.1.0: @@ -7484,6 +8360,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + totalist@3.0.1: {} ts-api-utils@2.5.0(typescript@5.8.3): @@ -7503,6 +8381,8 @@ snapshots: tslib@2.8.1: {} + tsscmp@1.0.6: {} + tsx@4.21.0: dependencies: esbuild: 0.25.12 @@ -7527,6 +8407,12 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@2.1.0: + dependencies: + content-type: 2.1.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -7595,6 +8481,10 @@ snapshots: undici-types@6.21.0: {} + undici@6.28.0: {} + + unpipe@1.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -7620,6 +8510,8 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + vary@1.1.2: {} + vite@7.3.6(@types/node@22.15.32)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0): dependencies: esbuild: 0.25.12 @@ -7755,6 +8647,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrappy@1.0.2: {} + ws@8.21.0: {} xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz: {} diff --git a/sites/mainweb/app/(portal)/api/webhooks/slack/route.ts b/sites/mainweb/app/(portal)/api/webhooks/slack/route.ts new file mode 100644 index 00000000..e2ed07cb --- /dev/null +++ b/sites/mainweb/app/(portal)/api/webhooks/slack/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { handleSlackWebhook } from "@query/dsgt-slack/http"; + +/** + * Slack Events API, slash commands, and interactivity. + * + * Request URL (Event Subscriptions, Slash Commands, Interactivity): + * {AUTH_URL}/api/webhooks/slack + * AUTH_URL / NEXTAUTH_URL in apphosting.yaml is the App Hosting origin + * (club host datasciencegt.org). Do not point Slack at the static Firebase + * Hosting site (`dsgt-website` / `sites/mainweb/out`). Signature checks use + * SLACK_SIGNING_SECRET. + */ +export async function POST(req: NextRequest) { + const botToken = process.env.SLACK_BOT_TOKEN; + const signingSecret = process.env.SLACK_SIGNING_SECRET; + + if (!botToken || !signingSecret) { + console.error( + "Slack webhook missing SLACK_BOT_TOKEN or SLACK_SIGNING_SECRET.", + ); + return NextResponse.json( + { error: "Server configuration error" }, + { status: 500 }, + ); + } + + return handleSlackWebhook(req, { botToken, signingSecret }); +} diff --git a/sites/mainweb/next.config.mjs b/sites/mainweb/next.config.mjs index 42084647..f53e7525 100644 --- a/sites/mainweb/next.config.mjs +++ b/sites/mainweb/next.config.mjs @@ -50,7 +50,14 @@ const cspHeaderName = const nextConfig = { output: "standalone", reactCompiler: true, - transpilePackages: ["@query/api", "@query/auth", "@query/db", "@query/ui"], + transpilePackages: [ + "@query/api", + "@query/auth", + "@query/db", + "@query/dsgt-slack", + "@query/ui", + ], + serverExternalPackages: ["@slack/web-api", "@slack/bolt"], outputFileTracingRoot: path.join(__dirname, "../../"), async headers() { return [ diff --git a/sites/mainweb/package.json b/sites/mainweb/package.json index 92515417..279fda9e 100644 --- a/sites/mainweb/package.json +++ b/sites/mainweb/package.json @@ -16,6 +16,7 @@ "@query/api": "workspace:*", "@query/auth": "workspace:*", "@query/db": "workspace:*", + "@query/dsgt-slack": "workspace:*", "@query/ui": "workspace:*", "@stripe/react-stripe-js": "^3.7.0", "@stripe/stripe-js": "^5.8.0", diff --git a/turbo.json b/turbo.json index 8cbe4787..c08f4eaf 100644 --- a/turbo.json +++ b/turbo.json @@ -81,7 +81,11 @@ "DDOS_BLOCK_DURATION_MS", "DDOS_BURST_THRESHOLD", "DDOS_BURST_WINDOW_MS", - "DDOS_CLEANUP_INTERVAL_MS" + "DDOS_CLEANUP_INTERVAL_MS", + "SLACK_BOT_TOKEN", + "SLACK_SIGNING_SECRET", + "SLACK_APP_TOKEN", + "SLACK_SOCKET_MODE" ], "globalPassThroughEnv": ["NODE_ENV", "CI", "npm_lifecycle_event"] } From faaa8b3fb2b9750625e926ee999b7eae41092a53 Mon Sep 17 00:00:00 2001 From: aamoghS Date: Mon, 17 Aug 2026 16:59:36 +0000 Subject: [PATCH 2/4] Document that Grok Bot is separate from the FAQ Slack webhook The Firebase @dsgt app only answers help, ping, and join. Google Slides, Forms, and other MCP tools belong on the Grok Bot teammate installed into the DS@GT Slack workspace. --- apps/dsgt-slack/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/dsgt-slack/README.md b/apps/dsgt-slack/README.md index 7aa1fe82..55ca0ad5 100644 --- a/apps/dsgt-slack/README.md +++ b/apps/dsgt-slack/README.md @@ -13,6 +13,27 @@ Production traffic is **HTTP Events API** on the existing Firebase App Hosting backend (`sites/mainweb` / Cloud Run). There is no second Cloud Run service and no Socket Mode process in production. +## Grok Bot (the LLM / MCP teammate) + +This Firebase app is **not** Grok Bot. It is a small FAQ process: help, ping, +and the August 26 join answer. It cannot use your Grok Bot computer, Google +connectors, or MCP tools. + +If club Slack should talk to **your Grok Bot** (the Yodo pattern: people DM +`@dsgt`, and the teammate uses Slack, Drive, Slides, Forms, Gmail), do that in +the Grok Bot app, not here: + +1. Open Grok Bot and open the teammate you want as `@dsgt`. +2. Connect **Slack** to the **DS@GT club workspace** (not Campus Leads). +3. When Slack asks to install the app, click **Allow** as a workspace admin. +4. Add Google Workspace connectors there (Drive / Slides / Forms). MCP for + those products lives on Grok Bot. +5. Name the Slack bot user `dsgt` so it matches the club identity. + +You can keep this FAQ webhook as a fallback, or skip installing it if Grok Bot +is the only `@dsgt` in the workspace. Do not run both against the same bot +tokens or they will fight over events. + ## Create the Slack app from the manifest 1. Open [https://api.slack.com/apps](https://api.slack.com/apps) and sign in to From ef0d6df55246076f693a5b28bc09ee9f8eff6dbc Mon Sep 17 00:00:00 2001 From: aamoghS Date: Mon, 17 Aug 2026 17:00:54 +0000 Subject: [PATCH 3/4] Avoid ReDoS when stripping Slack mentions Replace the polynomial <@[^>]+> matcher with a linear indexOf scan so a flood of incomplete <@ prefixes cannot backtrack on event text. --- apps/dsgt-slack/src/replies.test.ts | 12 ++++++++++ apps/dsgt-slack/src/replies.ts | 37 ++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/apps/dsgt-slack/src/replies.test.ts b/apps/dsgt-slack/src/replies.test.ts index da3d8a20..5c247a88 100644 --- a/apps/dsgt-slack/src/replies.test.ts +++ b/apps/dsgt-slack/src/replies.test.ts @@ -6,6 +6,7 @@ import { detectIntent, replyForSlashCommand, replyForText, + stripBotMention, } from "./replies"; describe("join FAQ reply", () => { @@ -40,3 +41,14 @@ describe("slash command routing", () => { expect(replyForSlashCommand("join")).toBe(JOIN_FAQ_REPLY); }); }); + +describe("stripBotMention", () => { + it("removes a Slack user mention before the rest of the text", () => { + expect(stripBotMention("<@U123> how do I join?")).toBe("how do I join?"); + }); + + it("is linear on a long run of incomplete <@ prefixes", () => { + const noisy = `${"<@".repeat(200)} how do I join?`; + expect(detectIntent(noisy)).toBe("join"); + }); +}); diff --git a/apps/dsgt-slack/src/replies.ts b/apps/dsgt-slack/src/replies.ts index 14a4d15d..db01ff29 100644 --- a/apps/dsgt-slack/src/replies.ts +++ b/apps/dsgt-slack/src/replies.ts @@ -32,8 +32,6 @@ export const DEFAULT_REPLY = export const UNKNOWN_COMMAND_REPLY = "I did not recognize that request. Send help for the topics I can answer."; -const BOT_MENTION = /<@[^>]+>/g; - const JOIN_PATTERN = /\b(join|sign[ -]?up|membership|member|get involved|first (event|meeting)|8\/26|august 26|26th)\b/i; @@ -43,8 +41,41 @@ const HELP_PATTERN = /\b(help|commands|what can you do)\b/i; export type ReplyIntent = "join" | "health" | "help" | "default"; +/** + * Slack mentions are `<@U…>` (optionally `<@U…|label>`). Walk the string once + * with indexOf so a flood of `<@` without a closing `>` cannot backtrack. + */ export function stripBotMention(text: string): string { - return text.replace(BOT_MENTION, " ").replace(/\s+/g, " ").trim(); + let output = ""; + let i = 0; + while (i < text.length) { + const start = text.indexOf("<@", i); + if (start === -1) { + output += text.slice(i); + break; + } + output += text.slice(i, start); + const end = text.indexOf(">", start + 2); + if (end === -1) { + output += text.slice(start); + break; + } + output += " "; + i = end + 1; + } + + let collapsed = ""; + for (const ch of output) { + const isSpace = ch === " " || ch === "\t" || ch === "\n" || ch === "\r"; + if (isSpace) { + if (collapsed.length > 0 && collapsed[collapsed.length - 1] !== " ") { + collapsed += " "; + } + } else { + collapsed += ch; + } + } + return collapsed.trim(); } export function detectIntent(text: string): ReplyIntent { From c91f4cff0b84695003a879bfaefa11701202670e Mon Sep 17 00:00:00 2001 From: aamoghS Date: Mon, 17 Aug 2026 17:43:54 +0000 Subject: [PATCH 4/4] Fix Slack app manifest so Create from manifest succeeds Slack rejects Event Subscriptions, Interactivity, and /dsgt when Socket Mode is off and no Request URL is set. Keep the @dsgt bot user and scopes so the app can be installed; add those HTTP features in the Slack UI after the webhook is live. --- apps/dsgt-slack/README.md | 13 +++++++++---- apps/dsgt-slack/manifest.yaml | 12 +----------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/apps/dsgt-slack/README.md b/apps/dsgt-slack/README.md index 55ca0ad5..ed56d007 100644 --- a/apps/dsgt-slack/README.md +++ b/apps/dsgt-slack/README.md @@ -42,8 +42,10 @@ tokens or they will fight over events. 3. Select the workspace, then paste the contents of [`manifest.yaml`](./manifest.yaml) (YAML is accepted). 4. Confirm the summary. The app name and bot user display name should both be - `dsgt`. Socket Mode should be **off**. Request URLs are left unset in the - manifest so you can create the app before the webhook is deployed. + `dsgt`. Socket Mode should be **off**. The manifest does **not** declare + Event Subscriptions, Interactivity, or `/dsgt` — Slack rejects those without + a Request URL or Socket Mode (`invalid_manifest`). You can add them in the + app settings after `{AUTH_URL}/api/webhooks/slack` is live. 5. Click **Create**. You now have a real Slack app whose bot user is **dsgt**. @@ -77,8 +79,11 @@ for the club site, host `datasciencegt.org`) and append the path: ``` Set those three URLs in the Slack app settings **after** the App Hosting -deploy that includes this route is live. Slack will POST a -`url_verification` challenge; the Next.js route answers it. +deploy that includes this route is live (Event Subscriptions, Slash Commands +`/dsgt`, Interactivity). They are not in `manifest.yaml` because Slack will +not create the app with those features unless a Request URL or Socket Mode is +set. Slack will POST a `url_verification` challenge; the Next.js route +answers it. The Next.js route is `sites/mainweb/app/(portal)/api/webhooks/stripe`’s neighbor: diff --git a/apps/dsgt-slack/manifest.yaml b/apps/dsgt-slack/manifest.yaml index 0508fc0d..62ffdeea 100644 --- a/apps/dsgt-slack/manifest.yaml +++ b/apps/dsgt-slack/manifest.yaml @@ -13,27 +13,17 @@ features: bot_user: display_name: dsgt always_online: true - slash_commands: - - command: /dsgt - description: Ask the Data Science @ Georgia Tech bot for help. - usage_hint: "[help | ping | join]" - should_escape: false oauth_config: scopes: bot: - app_mentions:read - chat:write + - chat:write.public - commands - im:history - im:read - im:write settings: - event_subscriptions: - bot_events: - - app_mention - - message.im - interactivity: - is_enabled: true org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false