diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..c0f8959
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,6 @@
+node_modules
+dist
+coverage
+.git
+.env
+*.log
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..41c099a
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,19 @@
+# Slack request verification
+SLACK_SIGNING_SECRET=
+
+# Local, single-workspace development
+SLACK_BOT_TOKEN=
+
+# OAuth distribution (optional for local development)
+SLACK_CLIENT_ID=
+SLACK_CLIENT_SECRET=
+SLACK_STATE_SECRET=replace-with-at-least-32-random-characters
+TOKEN_ENCRYPTION_KEY=
+
+# Runtime
+DATABASE_URL=postgresql://teamloop:teamloop@localhost:5432/teamloop?schema=public
+APP_BASE_URL=http://localhost:3000
+PORT=3000
+NODE_ENV=development
+LOG_LEVEL=info
+DEFAULT_TIMEZONE=UTC
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..9a677b1
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @ayushhagarwal
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..4a295a2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,23 @@
+---
+name: Bug report
+about: Report a reproducible TeamLoop problem
+title: "[Bug] "
+labels: bug
+---
+
+## What happened?
+
+## Steps to reproduce
+
+## Expected behavior
+
+## Environment
+
+- TeamLoop commit/version:
+- Node version:
+- Deployment method:
+- Slack surface and command:
+
+## Safe logs
+
+Remove tokens, workspace identifiers, channel names, and personal data.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..8269108
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: TeamLoop discussions
+ url: https://github.com/ayushhagarwal/teamloop/discussions
+ about: Ask setup questions, share ideas, and show how your team uses TeamLoop.
+ - name: Security report
+ url: https://github.com/ayushhagarwal/teamloop/security/advisories/new
+ about: Privately report a security vulnerability.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..622a468
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,14 @@
+---
+name: Feature request
+about: Propose a focused improvement
+title: "[Feature] "
+labels: enhancement
+---
+
+## Team coordination problem
+
+## Proposed Slack experience
+
+## Why this belongs in the current roadmap
+
+## Privacy or scope considerations
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..ca9742d
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,24 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: /
+ schedule:
+ interval: weekly
+ day: monday
+ time: "06:00"
+ timezone: Asia/Kolkata
+ open-pull-requests-limit: 5
+ labels:
+ - dependencies
+ groups:
+ development-dependencies:
+ dependency-type: development
+ production-dependencies:
+ dependency-type: production
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ labels:
+ - dependencies
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..71d28c3
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,16 @@
+## What changed?
+
+## Why?
+
+## Verification
+
+- [ ] `npm run format:check`
+- [ ] `npm run lint`
+- [ ] `npm run typecheck`
+- [ ] `npm test`
+- [ ] Manual Slack interaction tested where relevant
+
+## Privacy and scope
+
+- [ ] No secrets or real workspace data included
+- [ ] No V1 scope creep
diff --git a/.github/release.yml b/.github/release.yml
new file mode 100644
index 0000000..993c263
--- /dev/null
+++ b/.github/release.yml
@@ -0,0 +1,25 @@
+changelog:
+ exclude:
+ labels:
+ - skip-changelog
+ categories:
+ - title: Breaking changes
+ labels:
+ - breaking-change
+ - title: New features
+ labels:
+ - enhancement
+ - feature
+ - title: Fixes
+ labels:
+ - bug
+ - fix
+ - title: Documentation
+ labels:
+ - documentation
+ - title: Dependencies
+ labels:
+ - dependencies
+ - title: Other changes
+ labels:
+ - "*"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..fb17ae8
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,23 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ quality:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ - run: npm ci
+ - run: npm run prisma:generate
+ - run: npm run format:check
+ - run: npm run lint
+ - run: npm run typecheck
+ - run: npm test
+ - run: npm run build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..2bb3299
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,70 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: Semantic version without the v prefix, for example 1.1.0
+ required: true
+ type: string
+
+permissions:
+ contents: write
+
+concurrency:
+ group: release
+ cancel-in-progress: false
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Validate release version
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$'; then
+ echo "Version must be valid semver without a v prefix."
+ exit 1
+ fi
+ PACKAGE_VERSION="$(node -p "require('./package.json').version")"
+ if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
+ echo "package.json version $PACKAGE_VERSION does not match $VERSION."
+ exit 1
+ fi
+ if git rev-parse "v$VERSION" >/dev/null 2>&1; then
+ echo "Tag v$VERSION already exists."
+ exit 1
+ fi
+
+ - run: npm ci
+ - run: npm run prisma:generate
+ - run: npm run format:check
+ - run: npm run lint
+ - run: npm run typecheck
+ - run: npm test
+ - run: npm run build
+
+ - name: Create annotated tag
+ env:
+ VERSION: ${{ inputs.version }}
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git tag -a "v$VERSION" -m "TeamLoop v$VERSION"
+ git push origin "v$VERSION"
+
+ - name: Create GitHub release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ VERSION: ${{ inputs.version }}
+ run: gh release create "v$VERSION" --title "TeamLoop v$VERSION" --generate-notes
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..73c7b74
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+dist/
+coverage/
+.env
+.env.*
+!.env.example
+*.log
+.DS_Store
+prisma/dev.db
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..68f9def
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,4 @@
+node_modules
+dist
+coverage
+package-lock.json
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..6bb3bf2
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "semi": true,
+ "singleQuote": false,
+ "trailingComma": "all",
+ "printWidth": 88
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..f11c950
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,20 @@
+# Changelog
+
+All notable TeamLoop changes are documented through GitHub Releases and this
+file. The project follows [Semantic Versioning](https://semver.org/).
+
+## [1.0.0] - 2026-06-21
+
+### Added
+
+- Six Slack activity commands: lunch, dinner, potluck, weekend run, marathon,
+ and HYROX.
+- Interactive event cards with live RSVPs, preferences, summaries, and
+ organizer controls.
+- Potluck contribution tracking and still-needed categories.
+- Scheduled and manual reminders with duplicate-send protection.
+- PostgreSQL and Prisma persistence, Slack OAuth distribution, and encrypted
+ workspace token storage.
+- Docker Compose, automated tests, CI, and open-source project documentation.
+
+[1.0.0]: https://github.com/ayushhagarwal/teamloop/releases/tag/v1.0.0
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..3d344c8
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,20 @@
+# Code of Conduct
+
+## Our pledge
+
+We pledge to make participation in TeamLoop welcoming and harassment-free for
+everyone, regardless of background, identity, experience, or ability.
+
+## Expected behavior
+
+Be respectful, assume good intent, give actionable feedback, and keep technical
+disagreement focused on the work. Harassment, discrimination, threats, and
+publishing another person’s private information are not acceptable.
+
+## Enforcement
+
+Report conduct or safety concerns privately to the repository maintainers.
+Maintainers may edit or remove contributions and may temporarily or permanently
+restrict participation when behavior harms the community.
+
+This policy is adapted from the Contributor Covenant, version 2.1.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..a5c1258
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,23 @@
+# Contributing to TeamLoop
+
+Thanks for helping teams plan things without a 47-message RSVP thread.
+
+## Development workflow
+
+1. Read [docs/local-development.md](docs/local-development.md).
+2. Create a focused branch from `main`.
+3. Add or update tests with behavior changes.
+4. Run `npm run format:check`, `npm run lint`, `npm run typecheck`, and `npm test`.
+5. Open a pull request using the repository template.
+
+Keep V1 changes small, privacy-conscious, and Slack-first. New dependencies
+should be open source and should not make a paid service mandatory.
+
+## Commit and pull request guidance
+
+- Explain the user problem, not only the code change.
+- Include manual Slack testing notes for interaction changes.
+- Avoid unrelated formatting or refactors.
+- Never include workspace tokens, signing secrets, user data, or production logs.
+
+By participating, you agree to follow the [Code of Conduct](CODE_OF_CONDUCT.md).
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..23c7f17
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,21 @@
+FROM node:20-alpine AS build
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm ci
+COPY prisma ./prisma
+COPY prisma.config.ts ./prisma.config.ts
+RUN npm run prisma:generate
+COPY tsconfig.json vitest.config.ts eslint.config.js .prettierrc.json ./
+COPY src ./src
+RUN npm run build
+
+FROM node:20-alpine AS runtime
+WORKDIR /app
+ENV NODE_ENV=production
+COPY --from=build /app/node_modules ./node_modules
+COPY --from=build /app/dist ./dist
+COPY --from=build /app/prisma ./prisma
+COPY --from=build /app/prisma.config.ts ./prisma.config.ts
+COPY package.json package-lock.json ./
+EXPOSE 3000
+CMD ["sh", "-c", "npm run prisma:migrate && npm start"]
diff --git a/LICENSE b/LICENSE
index 491c920..d182166 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2026 Ayush Kumar Agarwal
+Copyright (c) 2026 Ayush Kumar Agarwal and TeamLoop contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..04ce528
--- /dev/null
+++ b/README.md
@@ -0,0 +1,203 @@
+
+
+
+
+# TeamLoop
+
+Plan team lunches, potlucks, runs, and fitness sessions directly in Slack.
+
+[](https://github.com/ayushhagarwal/teamloop/actions/workflows/ci.yml)
+[](https://github.com/ayushhagarwal/teamloop/releases)
+[](LICENSE)
+[](https://github.com/ayushhagarwal/teamloop/issues)
+
+TeamLoop is an open-source Slack bot for planning real-world team activities —
+lunches, dinners, potlucks, weekend runs, marathon training, and HYROX sessions.
+It creates interactive Slack event cards with RSVPs, preferences, reminders,
+organizer controls, and live-updating summaries.
+
+> V1 is Slack-only, self-hostable, and deliberately small. There is no billing,
+> dashboard, AI, calendar integration, or paid service dependency.
+
+## What it does
+
+- Creates an activity in under a minute with a slash command and modal.
+- Keeps `Going`, `Maybe`, and `Can’t make it` counts current in one message.
+- Collects food, pace, race, partner, beginner-group, and HYROX preferences.
+- Tracks potluck contributions and categories that are still needed.
+- Gives organizers edit, reminder, close-RSVP, cancel, and summary controls.
+- Schedules idempotent RSVP-deadline and event-start reminders.
+- Enforces organizer actions server-side and stores only coordination data.
+
+| Command | Activity-specific support |
+| ------------- | ------------------------------------------------------------ |
+| `/lunch` | Budget and food preference |
+| `/dinner` | Budget, plus-one, and food preference |
+| `/potluck` | Needed categories and contribution claims |
+| `/weekendrun` | Distance, pace, route, and beginner group |
+| `/marathon` | Race/training details, distance, target pace, beginner group |
+| `/hyrox` | Workout type, partner need, beginner support, and intensity |
+
+## Preview
+
+```text
+🍱 Team Lunch
+Created by @ayush · Lunch · Status: Open
+
+📅 Friday at 12:30 PM 📍 Green Cafe
+⏰ RSVP by Friday 10 AM ✅ 3 · 🤔 2 · ❌ 1
+
+🍴 Lunch details
+Budget: ₹500
+Preferences: Veg 2 · Vegan 1
+
+[✅ Going] [🤔 Maybe] [❌ Can’t make it] [Set food preference] [View summary]
+```
+
+Add sanitized product images to [`screenshots/`](screenshots/README.md) and a
+demo GIF to [`demo/`](demo/README.md).
+
+## Quick start with Docker
+
+Requirements: Docker, Docker Compose, a Slack development workspace, and a
+public HTTPS tunnel such as ngrok.
+
+```bash
+cp .env.example .env
+# Add SLACK_SIGNING_SECRET and SLACK_BOT_TOKEN to .env
+docker compose up --build
+```
+
+The app listens on `http://localhost:3000`; its health endpoint is
+`GET /health`. Expose it while developing:
+
+```bash
+ngrok http 3000
+```
+
+Set Slack slash commands, interactivity, and event subscriptions to:
+
+```text
+https://YOUR_NGROK_DOMAIN/slack/events
+```
+
+Follow the full [Slack app setup guide](docs/slack-app-setup.md).
+
+## Local development
+
+```bash
+npm install
+docker compose up -d postgres
+cp .env.example .env
+npm run prisma:migrate
+npm run dev
+```
+
+Useful commands:
+
+```bash
+npm run prisma:migrate:dev
+npm run prisma:seed
+npm run format:check
+npm run lint
+npm run typecheck
+npm test
+npm run build
+```
+
+See [local development](docs/local-development.md) and
+[self-hosting](docs/self-hosting.md) for complete instructions.
+
+## Slack installation modes
+
+For one development workspace, set `SLACK_BOT_TOKEN`.
+
+For distributed OAuth installs, set `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`,
+`SLACK_STATE_SECRET`, `TOKEN_ENCRYPTION_KEY`, and a public `APP_BASE_URL`.
+TeamLoop then exposes:
+
+- `/slack/install`
+- `/slack/oauth_redirect`
+- `/slack/events`
+
+Workspace bot tokens are encrypted with AES-256-GCM when
+`TOKEN_ENCRYPTION_KEY` is configured.
+
+## Environment variables
+
+| Variable | Required | Purpose |
+| ---------------------- | ---------- | ------------------------------------------ |
+| `DATABASE_URL` | Yes | PostgreSQL connection URL |
+| `SLACK_SIGNING_SECRET` | Yes | Verifies requests from Slack |
+| `SLACK_BOT_TOKEN` | Local mode | Single-workspace bot token |
+| `SLACK_CLIENT_ID` | OAuth mode | Slack app client ID |
+| `SLACK_CLIENT_SECRET` | OAuth mode | Slack app client secret |
+| `SLACK_STATE_SECRET` | OAuth mode | OAuth state signing secret, 32+ characters |
+| `TOKEN_ENCRYPTION_KEY` | OAuth mode | Base64-encoded 32-byte key |
+| `APP_BASE_URL` | OAuth mode | Public HTTPS origin |
+| `PORT` | No | HTTP port; default `3000` |
+| `DEFAULT_TIMEZONE` | No | Reserved for richer V1.1 timezone handling |
+| `LOG_LEVEL` | No | `debug`, `info`, `warn`, or `error` |
+
+V1 modal times are labeled and interpreted as UTC. Slack renders stored UTC
+timestamps in each viewer’s local timezone.
+
+## Architecture
+
+```text
+Slack command/action
+ → Bolt listener (acknowledges immediately)
+ → Zod validation
+ → Prisma service + PostgreSQL
+ → reusable Block Kit renderer
+ → chat.postMessage / chat.update
+
+node-cron
+ → atomically claims due reminder
+ → sends one Slack message
+ → records SENT or FAILED
+```
+
+The source is split by Slack surface (`commands`, `modals`, `blocks`,
+`interactions`) and domain service (`event`, `rsvp`, `reminder`, `potluck`).
+
+## Privacy
+
+TeamLoop stores workspace and channel IDs, event details, Slack user IDs,
+RSVPs, preferences, potluck items, reminders, and a small audit trail. It does
+not store message history, private conversations, emails, or Slack profiles.
+See [permissions and data](docs/permissions-and-data.md).
+
+## Deployment
+
+The included image runs Prisma migrations before starting the HTTP process.
+Use HTTPS, persistent PostgreSQL storage, encrypted backups, and one scheduler
+instance (or add distributed locking before scaling schedulers horizontally).
+See [self-hosting](docs/self-hosting.md).
+
+## Roadmap
+
+- **V1.1:** preference and potluck item editing, event templates, better
+ timezone support, App Home summary.
+- **V1.2:** expense splitting, calendar export/integration, recurring
+ activities, richer reminder controls.
+- **V1.3:** hosted version, install landing page, workspace billing, admin
+ dashboard.
+- **V2:** Microsoft Teams, AI suggestions, restaurant discovery, advanced
+ analytics, Marketplace polish.
+
+Roadmap items are not part of this V1 implementation.
+
+## Contributing and security
+
+Read [CONTRIBUTING.md](CONTRIBUTING.md), [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md),
+[SECURITY.md](SECURITY.md), and [SUPPORT.md](SUPPORT.md) before opening a
+contribution, support request, or security report.
+
+Releases use semantic version tags such as `v1.0.0`. Maintainers can create a
+tested tag and GitHub release from the **Release** workflow. See
+[the release guide](docs/releasing.md).
+
+## License
+
+[MIT](LICENSE)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..3627c63
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,25 @@
+# Security Policy
+
+## Reporting a vulnerability
+
+Please do not open a public issue for a vulnerability involving Slack tokens,
+request verification, authorization, data exposure, or injection. Send a
+private report to the security contact listed by the repository owner with:
+
+- affected version or commit;
+- reproduction steps;
+- likely impact;
+- any suggested mitigation.
+
+Maintainers should acknowledge reports within seven days. Do not include real
+workspace tokens or personal data in a report.
+
+## Supported versions
+
+Security fixes target the latest release on `main`.
+
+## Deployment notes
+
+Use HTTPS, a strong `SLACK_STATE_SECRET`, and a base64-encoded 32-byte
+`TOKEN_ENCRYPTION_KEY` for OAuth installs. Rotate any credential that may have
+been exposed and remove it from Git history.
diff --git a/SUPPORT.md b/SUPPORT.md
new file mode 100644
index 0000000..9ec95a2
--- /dev/null
+++ b/SUPPORT.md
@@ -0,0 +1,22 @@
+# Support
+
+## Getting help
+
+- Read the [README](README.md) and [local development guide](docs/local-development.md).
+- Search [existing issues](https://github.com/ayushhagarwal/teamloop/issues).
+- Open a [GitHub Discussion](https://github.com/ayushhagarwal/teamloop/discussions)
+ for setup questions and ideas.
+- Open a bug issue when behavior is reproducible.
+
+Please include the TeamLoop version, Node.js version, deployment method, Slack
+surface involved, expected behavior, and safe reproduction steps.
+
+## Security
+
+Do not report vulnerabilities or expose Slack tokens in a public issue. Follow
+[SECURITY.md](SECURITY.md).
+
+## Scope
+
+Community support is best-effort. TeamLoop V1 does not include a hosted service,
+payments, Microsoft Teams, a web dashboard, or private support tooling.
diff --git a/assets/logo-placeholder.svg b/assets/logo-placeholder.svg
new file mode 100644
index 0000000..41fe0d7
--- /dev/null
+++ b/assets/logo-placeholder.svg
@@ -0,0 +1,9 @@
+
diff --git a/demo/README.md b/demo/README.md
new file mode 100644
index 0000000..c7c0954
--- /dev/null
+++ b/demo/README.md
@@ -0,0 +1,4 @@
+# Demo assets
+
+Place the README demo GIF or short video here. Use a dedicated Slack test
+workspace with fictional users and events.
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..813b69e
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,34 @@
+services:
+ postgres:
+ image: postgres:16-alpine
+ environment:
+ POSTGRES_DB: teamloop
+ POSTGRES_USER: teamloop
+ POSTGRES_PASSWORD: teamloop
+ ports:
+ - "5432:5432"
+ volumes:
+ - teamloop-postgres:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U teamloop -d teamloop"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+
+ app:
+ build: .
+ env_file:
+ - .env
+ environment:
+ DATABASE_URL: postgresql://teamloop:teamloop@postgres:5432/teamloop?schema=public
+ NODE_ENV: production
+ PORT: 3000
+ ports:
+ - "3000:3000"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ restart: unless-stopped
+
+volumes:
+ teamloop-postgres:
diff --git a/docs/local-development.md b/docs/local-development.md
new file mode 100644
index 0000000..2532fa7
--- /dev/null
+++ b/docs/local-development.md
@@ -0,0 +1,100 @@
+# Local development
+
+## Prerequisites
+
+- Node.js 20+
+- Docker and Docker Compose
+- A Slack development workspace
+- ngrok or another HTTPS tunnel
+
+## Install and configure
+
+```bash
+npm install
+cp .env.example .env
+docker compose up -d postgres
+npm run prisma:generate
+npm run prisma:migrate
+```
+
+Add the signing secret and development workspace bot token to `.env`.
+
+## Run
+
+```bash
+npm run dev
+```
+
+In another terminal:
+
+```bash
+ngrok http 3000
+```
+
+Point all slash commands and Slack interactivity to
+`https://YOUR_DOMAIN/slack/events`.
+
+## Database changes
+
+Edit `prisma/schema.prisma`, then create a migration:
+
+```bash
+npm run prisma:migrate:dev -- --name describe_the_change
+```
+
+Never edit an already-deployed migration. The seed command creates only a demo
+workspace record:
+
+```bash
+npm run prisma:seed
+```
+
+## Quality checks
+
+```bash
+npm run format:check
+npm run lint
+npm run typecheck
+npm test
+npm run build
+```
+
+## Manual Slack checklist
+
+1. Run `/lunch` and create a future event.
+2. Click `Going`.
+3. Respond `Maybe` from another user.
+4. Set a food preference.
+5. Send an organizer reminder.
+6. Close RSVPs and confirm a new response is rejected.
+7. Cancel the event and confirm the card changes.
+8. Run `/dinner` and test a plus-one preference.
+9. Run `/potluck`.
+10. Claim a potluck item and confirm the user is marked going.
+11. Confirm claimed and still-needed categories update.
+12. Run `/weekendrun` and set a pace preference.
+13. Run `/marathon` and save a target pace note.
+14. Run `/hyrox` and save a HYROX preference.
+15. Edit an organizer-owned event.
+16. Try an organizer control from another user.
+17. Open the full summary.
+18. Confirm a scheduled reminder sends only once.
+19. Inspect logs for useful context without Slack tokens.
+20. Confirm the event, RSVP, preference, item, and reminder rows in PostgreSQL.
+
+## Time handling
+
+V1 modal labels explicitly use UTC. Values are converted to UTC `Date` values
+and PostgreSQL stores them as timestamps. Slack date tokens render in the
+viewer’s local timezone. Workspace-aware modal defaults are a V1.1 item.
+
+## Common problems
+
+- **`invalid_auth`:** reinstall the app and update `SLACK_BOT_TOKEN`.
+- **`dispatch_failed` or timeout:** verify the ngrok URL and that interactions
+ use `/slack/events`.
+- **Card cannot post:** add `chat:write.public` or invite the app to the channel.
+- **Expired trigger ID:** make sure listeners acknowledge immediately and avoid
+ breakpoints before `views.open`.
+- **Database unavailable:** wait for `docker compose ps` to show PostgreSQL as
+ healthy and verify `DATABASE_URL`.
diff --git a/docs/marketplace-readiness.md b/docs/marketplace-readiness.md
new file mode 100644
index 0000000..5b23e74
--- /dev/null
+++ b/docs/marketplace-readiness.md
@@ -0,0 +1,47 @@
+# Slack Marketplace readiness
+
+TeamLoop V1 supports OAuth distribution, but a real submission still needs
+operational and listing work.
+
+## Product and installation
+
+- [ ] Stable production HTTPS origin and direct install URL.
+- [ ] OAuth install, reinstall, and uninstall tested across multiple workspaces.
+- [ ] Staging Slack app with the same granular scopes.
+- [ ] Friendly installation success and failure pages.
+- [ ] At least the currently required number of active workspace installs.
+ Slack’s current guidelines should be checked immediately before submission;
+ as of June 21, 2026, the published guideline says apps installed on fewer
+ than five active workspaces are not accepted.
+
+## Listing assets
+
+- [ ] Final app icon and brand-approved screenshots.
+- [ ] Short and long descriptions.
+- [ ] Demo video showing all six commands and organizer controls.
+- [ ] Support email and public support page.
+- [ ] Public privacy policy and terms of service.
+- [ ] Installation, configuration, and uninstall instructions.
+
+## Review notes
+
+- Explain why each of the three scopes is necessary.
+- Provide test-workspace installation instructions and fictional test data.
+- Demonstrate permission errors for non-organizers.
+- Demonstrate token encryption, request-signature verification, data deletion,
+ duplicate reminder prevention, and failure handling.
+- Verify the listing and product behavior match.
+- Add at least one additional app collaborator.
+
+## Maintenance
+
+- Monitor errors, uninstalls, support requests, and Slack platform changes.
+- Keep contact, privacy, support, and listing URLs working.
+- Use a staging app and resubmit material behavior or scope changes.
+- Revoke tokens and communicate clearly if the app is discontinued.
+
+Official references:
+
+- [Marketplace guidelines and requirements](https://docs.slack.dev/slack-marketplace/slack-marketplace-app-guidelines-and-requirements)
+- [Distribution and review process](https://docs.slack.dev/slack-marketplace/distributing-your-app-in-the-slack-marketplace)
+- [Marketplace review guide](https://docs.slack.dev/slack-marketplace/slack-marketplace-review-guide)
diff --git a/docs/permissions-and-data.md b/docs/permissions-and-data.md
new file mode 100644
index 0000000..b2ceb1f
--- /dev/null
+++ b/docs/permissions-and-data.md
@@ -0,0 +1,54 @@
+# Permissions and data
+
+TeamLoop follows a narrow, Slack-first data model.
+
+## Slack scopes
+
+| Scope | Why it is needed |
+| ------------------- | ----------------------------------------------------------------------------------- |
+| `commands` | Receives `/lunch`, `/dinner`, `/potluck`, `/weekendrun`, `/marathon`, and `/hyrox`. |
+| `chat:write` | Posts and updates event cards, reminders, and ephemeral summaries. |
+| `chat:write.public` | Posts the initial card in a public channel when the app has not been invited. |
+
+No user token scopes are requested. Private channels must invite the app.
+TeamLoop does not request message history, email, profile, direct-message,
+file, admin, or broad channel-read scopes.
+
+## Stored data
+
+- Slack workspace ID, optional workspace name, bot user ID, installer user ID.
+- OAuth bot token, encrypted with AES-256-GCM when an encryption key is set.
+- Slack channel ID and optional channel name.
+- Event title, type, time, location, notes, state, message timestamp, and
+ activity-specific metadata.
+- Slack user IDs associated with RSVPs, preferences, and potluck items.
+- Reminder state and a minimal event audit trail.
+
+## Data not stored
+
+- Slack message history or thread contents.
+- Private messages.
+- Email addresses or full user profiles.
+- Files, calendars, payment details, health records, or location history.
+- Analytics or behavior outside explicit TeamLoop interactions.
+
+## Logs
+
+Logs include event/reminder IDs and error context. Secret-shaped logger fields
+are redacted. Operators should still avoid adding raw Slack payloads to logs.
+
+## Uninstall and deletion
+
+When `app_uninstalled` is configured, TeamLoop removes the stored workspace
+token and cancels pending reminders. Event data remains for the self-hosting
+operator to manage.
+
+Delete one workspace and all related records:
+
+```sql
+DELETE FROM "Workspace" WHERE "slackTeamId" = 'T_WORKSPACE_ID';
+```
+
+Foreign-key cascades remove channels, events, RSVPs, preferences, potluck
+items, reminders, and audit logs. Operators should define a retention period
+and include backups in deletion procedures.
diff --git a/docs/releasing.md b/docs/releasing.md
new file mode 100644
index 0000000..f3264dd
--- /dev/null
+++ b/docs/releasing.md
@@ -0,0 +1,38 @@
+# Releasing TeamLoop
+
+TeamLoop uses Semantic Versioning and annotated Git tags:
+
+- `vMAJOR.MINOR.PATCH`
+- patch for compatible fixes;
+- minor for compatible functionality;
+- major for incompatible changes.
+
+## Prepare a release
+
+1. Create a branch from the latest `main`.
+2. Update `package.json` and `package-lock.json` to the release version.
+3. Move notable changes from the Unreleased section into a dated section in
+ `CHANGELOG.md`.
+4. Open a pull request and wait for CI and review.
+5. Rebase-merge the pull request.
+
+## Publish
+
+In GitHub Actions, open the **Release** workflow, choose **Run workflow** on
+`main`, and enter the version without a leading `v`, for example `1.1.0`.
+
+The workflow:
+
+1. verifies that it is running from `main`;
+2. checks that `package.json` has the requested version;
+3. runs formatting, linting, typechecking, tests, and the production build;
+4. creates and pushes annotated tag `v1.1.0`;
+5. creates a GitHub Release with generated categorized notes.
+
+The workflow refuses to overwrite an existing tag. Releases should never be
+created from an unmerged feature branch.
+
+## Hotfixes
+
+Prepare a normal pull request from the current `main`, update the patch version,
+rebase-merge it, then run the same workflow.
diff --git a/docs/self-hosting.md b/docs/self-hosting.md
new file mode 100644
index 0000000..7565c81
--- /dev/null
+++ b/docs/self-hosting.md
@@ -0,0 +1,55 @@
+# Self-hosting
+
+## Docker Compose
+
+```bash
+cp .env.example .env
+# Configure Slack credentials and public APP_BASE_URL
+docker compose up -d --build
+```
+
+The app container waits for PostgreSQL health, runs `prisma migrate deploy`,
+and starts TeamLoop. Persist the `teamloop-postgres` volume.
+
+## Production checklist
+
+- Terminate TLS at a reverse proxy or load balancer.
+- Keep `.env` and backups outside the repository.
+- Set a 32+ character OAuth state secret.
+- Set a base64-encoded 32-byte token encryption key.
+- Restrict database network access.
+- Monitor `/health`, process exits, Slack API errors, and failed reminders.
+- Back up PostgreSQL and test restore procedures.
+- Deploy migrations before or with application startup.
+
+## Scaling
+
+The HTTP app is stateless apart from PostgreSQL and may have multiple
+instances. The reminder claim is atomic, but V1 is best operated with one
+scheduler instance. At larger scale, separate the scheduler into a worker and
+add a PostgreSQL advisory lock or queue.
+
+## Backups
+
+Example logical backup:
+
+```bash
+docker compose exec -T postgres pg_dump -U teamloop teamloop > teamloop.sql
+```
+
+Protect backups as sensitive because they contain Slack user IDs, event
+locations, and encrypted workspace bot tokens.
+
+## Deployment targets
+
+Any platform that can run a Node.js container and PostgreSQL works. Configure a
+stable HTTPS origin, port 3000, health checks, persistent database storage, and
+the environment variables from `.env.example`. No paid platform is required.
+
+## Upgrade
+
+1. Back up PostgreSQL.
+2. Pull the desired release.
+3. Run `docker compose build`.
+4. Run `docker compose up -d`.
+5. Confirm `/health`, migrations, one slash command, and reminder delivery.
diff --git a/docs/slack-app-setup.md b/docs/slack-app-setup.md
new file mode 100644
index 0000000..a2dba45
--- /dev/null
+++ b/docs/slack-app-setup.md
@@ -0,0 +1,105 @@
+# Slack app setup
+
+This guide configures TeamLoop in HTTP mode. Socket Mode is intentionally not
+used because it is unsuitable for public Slack Marketplace distribution.
+
+## 1. Start TeamLoop and expose HTTPS
+
+Run the app on port 3000, then create a tunnel:
+
+```bash
+ngrok http 3000
+```
+
+Use the resulting HTTPS origin as `APP_BASE_URL`.
+
+## 2. Create the Slack app
+
+Create an app at [api.slack.com/apps](https://api.slack.com/apps), either from
+scratch or from [`manifest.yml`](../manifest.yml). Replace every
+`https://YOUR_DOMAIN` value with the tunnel or deployed origin.
+
+## 3. Add bot scopes
+
+Under **OAuth & Permissions**, add:
+
+- `commands` — receives the six slash commands.
+- `chat:write` — posts and updates event cards and reminders.
+- `chat:write.public` — allows a slash command to create a card in a public
+ channel where the bot has not already been invited.
+
+Private channels must explicitly include the app. TeamLoop does not request
+`users:read`, channel read scopes, direct-message scopes, or message history.
+
+## 4. Add slash commands
+
+Create each command with request URL
+`https://YOUR_DOMAIN/slack/events`:
+
+- `/lunch`
+- `/dinner`
+- `/potluck`
+- `/weekendrun`
+- `/marathon`
+- `/hyrox`
+
+## 5. Enable interactivity
+
+Under **Interactivity & Shortcuts**, enable interactivity and set the request
+URL to:
+
+```text
+https://YOUR_DOMAIN/slack/events
+```
+
+No shortcuts are required in V1.
+
+## 6. Optional uninstall event
+
+Under **Event Subscriptions**, enable events, use the same request URL, and add
+the `app_uninstalled` bot event. TeamLoop clears the stored OAuth token and
+cancels pending reminders when this event arrives.
+
+## 7. Install for local development
+
+Install the app into a test workspace. Copy the bot token and signing secret:
+
+```dotenv
+SLACK_SIGNING_SECRET=...
+SLACK_BOT_TOKEN=xoxb-...
+```
+
+Restart TeamLoop and run `/lunch` in a test channel.
+
+## 8. Configure OAuth distribution
+
+Set the redirect URL in Slack:
+
+```text
+https://YOUR_DOMAIN/slack/oauth_redirect
+```
+
+Configure:
+
+```dotenv
+SLACK_CLIENT_ID=...
+SLACK_CLIENT_SECRET=...
+SLACK_STATE_SECRET=a-random-secret-with-at-least-32-characters
+TOKEN_ENCRYPTION_KEY=BASE64_ENCODED_32_BYTE_KEY
+APP_BASE_URL=https://YOUR_DOMAIN
+```
+
+Generate an encryption key:
+
+```bash
+openssl rand -base64 32
+```
+
+Do not set `SLACK_BOT_TOKEN` in distributed mode. Open
+`https://YOUR_DOMAIN/slack/install` to start OAuth.
+
+## Manual smoke test
+
+Create each event type, submit an RSVP from two users, set a preference, update
+the event, send a reminder, close RSVPs, and cancel it. For a potluck, add a
+contribution and confirm that it appears in the live card.
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..066753b
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,29 @@
+import eslint from "@eslint/js";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ {
+ ignores: ["dist/**", "node_modules/**", "coverage/**", "eslint.config.js"],
+ },
+ eslint.configs.recommended,
+ ...tseslint.configs.recommendedTypeChecked,
+ {
+ languageOptions: {
+ parserOptions: {
+ projectService: true,
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ rules: {
+ "@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }],
+ "@typescript-eslint/require-await": "off",
+ },
+ },
+ {
+ files: ["src/tests/**/*.ts"],
+ rules: {
+ "@typescript-eslint/no-unsafe-assignment": "off",
+ "@typescript-eslint/unbound-method": "off",
+ },
+ },
+);
diff --git a/manifest.yml b/manifest.yml
new file mode 100644
index 0000000..b14b499
--- /dev/null
+++ b/manifest.yml
@@ -0,0 +1,52 @@
+display_information:
+ name: TeamLoop
+ description: Plan team lunches, potlucks, runs, and fitness sessions directly in Slack.
+ background_color: "#5F9F68"
+features:
+ bot_user:
+ display_name: TeamLoop
+ always_online: false
+ slash_commands:
+ - command: /lunch
+ description: Plan a team lunch
+ usage_hint: ""
+ should_escape: false
+ - command: /dinner
+ description: Plan a team dinner
+ usage_hint: ""
+ should_escape: false
+ - command: /potluck
+ description: Plan a team potluck
+ usage_hint: ""
+ should_escape: false
+ - command: /weekendrun
+ description: Plan a weekend run
+ usage_hint: ""
+ should_escape: false
+ - command: /marathon
+ description: Plan marathon training or a race
+ usage_hint: ""
+ should_escape: false
+ - command: /hyrox
+ description: Plan a HYROX session
+ usage_hint: ""
+ should_escape: false
+oauth_config:
+ redirect_urls:
+ - https://YOUR_DOMAIN/slack/oauth_redirect
+ scopes:
+ bot:
+ - commands
+ - chat:write
+ - chat:write.public
+settings:
+ interactivity:
+ is_enabled: true
+ request_url: https://YOUR_DOMAIN/slack/events
+ event_subscriptions:
+ request_url: https://YOUR_DOMAIN/slack/events
+ bot_events:
+ - app_uninstalled
+ org_deploy_enabled: false
+ socket_mode_enabled: false
+ token_rotation_enabled: false
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..e0c8681
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,5106 @@
+{
+ "name": "teamloop",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "teamloop",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@prisma/client": "^6.10.1",
+ "@slack/bolt": "^4.4.0",
+ "@slack/oauth": "^3.0.5",
+ "@slack/types": "^2.21.1",
+ "@slack/web-api": "^7.17.0",
+ "dotenv": "^16.5.0",
+ "node-cron": "^4.1.1",
+ "zod": "^3.25.67"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.29.0",
+ "@types/node": "^22.15.32",
+ "@types/node-cron": "^3.0.11",
+ "eslint": "^9.29.0",
+ "prettier": "^3.5.3",
+ "prisma": "^6.10.1",
+ "tsx": "^4.20.3",
+ "typescript": "^5.8.3",
+ "typescript-eslint": "^8.34.1",
+ "vitest": "^4.1.9"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.133.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
+ "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@prisma/client": {
+ "version": "6.19.3",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz",
+ "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "peerDependencies": {
+ "prisma": "*",
+ "typescript": ">=5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/config": {
+ "version": "6.19.3",
+ "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz",
+ "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "c12": "3.1.0",
+ "deepmerge-ts": "7.1.5",
+ "effect": "3.21.0",
+ "empathic": "2.0.0"
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "6.19.3",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz",
+ "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/engines": {
+ "version": "6.19.3",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz",
+ "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.19.3",
+ "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
+ "@prisma/fetch-engine": "6.19.3",
+ "@prisma/get-platform": "6.19.3"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
+ "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
+ "devOptional": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "6.19.3",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz",
+ "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.19.3",
+ "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
+ "@prisma/get-platform": "6.19.3"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "6.19.3",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz",
+ "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/debug": "6.19.3"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
+ "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
+ "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
+ "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
+ "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
+ "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
+ "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
+ "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
+ "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
+ "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
+ "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
+ "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
+ "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
+ "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
+ "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
+ "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@slack/bolt": {
+ "version": "4.7.3",
+ "resolved": "https://registry.npmjs.org/@slack/bolt/-/bolt-4.7.3.tgz",
+ "integrity": "sha512-bODs8q/yNDWUPoxmQhFrRqLMA5vhB/PDizYWqb6CkQhLWEUo5JFtfJcmeU4ElGl6qSt++OKjSYNa4MPc77CleQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@slack/logger": "^4.0.1",
+ "@slack/oauth": "^3.0.5",
+ "@slack/socket-mode": "^2.0.7",
+ "@slack/types": "^2.21.1",
+ "@slack/web-api": "^7.16.0",
+ "axios": "^1.12.0",
+ "express": "^5.0.0",
+ "path-to-regexp": "^8.1.0",
+ "raw-body": "^3",
+ "tsscmp": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=18",
+ "npm": ">=8.6.0"
+ },
+ "peerDependencies": {
+ "@types/express": "^5.0.0"
+ }
+ },
+ "node_modules/@slack/logger": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz",
+ "integrity": "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">=18"
+ },
+ "engines": {
+ "node": ">= 18",
+ "npm": ">= 8.6.0"
+ }
+ },
+ "node_modules/@slack/oauth": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-3.0.5.tgz",
+ "integrity": "sha512-exqFQySKhNDptWYSWhvRUJ4/+ndu2gayIy7vg/JfmJq3wGtGdHk531P96fAZyBm5c1Le3yaPYqv92rL4COlU3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@slack/logger": "^4.0.1",
+ "@slack/web-api": "^7.15.0",
+ "@types/jsonwebtoken": "^9",
+ "@types/node": ">=18",
+ "jsonwebtoken": "^9"
+ },
+ "engines": {
+ "node": ">=18",
+ "npm": ">=8.6.0"
+ }
+ },
+ "node_modules/@slack/socket-mode": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@slack/socket-mode/-/socket-mode-2.0.7.tgz",
+ "integrity": "sha512-qYy07je71WnEHgRwmw12DlAnZLi5HXmdlI2WUzUK2LH/rYXQpP6uEg462S5CwfE8FoCKUdIigHtYnOOfzZH1lQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@slack/logger": "^4.0.1",
+ "@slack/web-api": "^7.15.0",
+ "@types/node": ">=18",
+ "@types/ws": "^8",
+ "eventemitter3": "^5",
+ "ws": "^8"
+ },
+ "engines": {
+ "node": ">= 18",
+ "npm": ">= 8.6.0"
+ }
+ },
+ "node_modules/@slack/types": {
+ "version": "2.21.1",
+ "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.21.1.tgz",
+ "integrity": "sha512-I8vmSjNYWsaxuWPx6dz4yeh0h7vRBWbgAMK14LEmblbZ404BtrPbXs6jDPx4cYgGf8msDGF4A9opLZBu21FViQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0",
+ "npm": ">= 6.12.0"
+ }
+ },
+ "node_modules/@slack/web-api": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.17.0.tgz",
+ "integrity": "sha512-jejr34a8B4L5AS713wOAx1LAqNkW16HVMDEa6sYBvFDc/llUBl8hXaiI4BwF+Al+Sug19Vn2O7iokTVIhVvZ1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@slack/logger": "^4.0.1",
+ "@slack/types": "^2.21.0",
+ "@types/node": ">=18",
+ "@types/retry": "0.12.0",
+ "axios": "^1.16.0",
+ "eventemitter3": "^5.0.1",
+ "form-data": "^4.0.4",
+ "is-electron": "2.2.2",
+ "is-stream": "^2",
+ "p-queue": "^6",
+ "p-retry": "^4",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">= 18",
+ "npm": ">= 8.6.0"
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "^2"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
+ "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/jsonwebtoken": {
+ "version": "9.0.10",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
+ "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz",
+ "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/node-cron": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz",
+ "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
+ "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.61.1",
+ "@typescript-eslint/type-utils": "8.61.1",
+ "@typescript-eslint/utils": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.61.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
+ "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.61.1",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
+ "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.61.1",
+ "@typescript-eslint/types": "^8.61.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
+ "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
+ "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
+ "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1",
+ "@typescript-eslint/utils": "8.61.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
+ "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
+ "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.61.1",
+ "@typescript-eslint/tsconfig-utils": "8.61.1",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
+ "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.61.1",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
+ "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.61.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.9",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
+ "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.5",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^2.0.0",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.1",
+ "iconv-lite": "^0.7.2",
+ "on-finished": "^2.4.1",
+ "qs": "^6.15.2",
+ "raw-body": "^3.0.2",
+ "type-is": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/body-parser/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/c12": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
+ "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.3",
+ "confbox": "^0.2.2",
+ "defu": "^6.1.4",
+ "dotenv": "^16.6.1",
+ "exsolve": "^1.0.7",
+ "giget": "^2.0.0",
+ "jiti": "^2.4.2",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "perfect-debounce": "^1.0.0",
+ "pkg-types": "^2.2.0",
+ "rc9": "^2.1.2"
+ },
+ "peerDependencies": {
+ "magicast": "^0.3.5"
+ },
+ "peerDependenciesMeta": {
+ "magicast": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/citty": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
+ "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "consola": "^3.2.3"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/confbox": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+ "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge-ts": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
+ "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
+ "devOptional": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/defu": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/effect": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz",
+ "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "fast-check": "^3.23.1"
+ }
+ },
+ "node_modules/empathic": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
+ "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/exsolve": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
+ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-check": {
+ "version": "3.23.2",
+ "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
+ "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "pure-rand": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/form-data/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/giget": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
+ "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "citty": "^0.1.6",
+ "consola": "^3.4.0",
+ "defu": "^6.1.4",
+ "node-fetch-native": "^1.6.6",
+ "nypm": "^0.6.0",
+ "pathe": "^2.0.3"
+ },
+ "bin": {
+ "giget": "dist/cli.mjs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-electron": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz",
+ "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==",
+ "license": "MIT"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "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.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.14",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz",
+ "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-cron": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.4.1.tgz",
+ "integrity": "sha512-1e4Gku1qeKx1ywNnMoA0o5hiZ8iA4klH84TIszTHZAME68xVIjLkiBo5KrtowbcOJn8MQqJG/yWHjC/dFdMWyQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/node-fetch-native": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
+ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/nypm": {
+ "version": "0.6.7",
+ "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.7.tgz",
+ "integrity": "sha512-s3ds97SD5pd1dULE+tHUk1DrV0cSHOnsfpcdGATJ8JpBo21DoKqN9exTH4/2nhPQNOLomBdTFMicN94S4DrZrQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "citty": "^0.2.2",
+ "pathe": "^2.0.3",
+ "tinyexec": "^1.2.4"
+ },
+ "bin": {
+ "nypm": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/nypm/node_modules/citty": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
+ "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/ohash": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-queue": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
+ "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.4",
+ "p-timeout": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-queue/node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "license": "MIT"
+ },
+ "node_modules/p-retry": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-timeout": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+ "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
+ "license": "MIT",
+ "dependencies": {
+ "p-finally": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
+ "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.2.4",
+ "exsolve": "^1.0.8",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.4",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz",
+ "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prisma": {
+ "version": "6.19.3",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz",
+ "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@prisma/config": "6.19.3",
+ "@prisma/engines": "6.19.3"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": ">=18.18"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pure-rand": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
+ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/rc9": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
+ "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "defu": "^6.1.4",
+ "destr": "^2.0.3"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
+ "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.133.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.3",
+ "@rolldown/binding-darwin-arm64": "1.0.3",
+ "@rolldown/binding-darwin-x64": "1.0.3",
+ "@rolldown/binding-freebsd-x64": "1.0.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.3",
+ "@rolldown/binding-linux-arm64-musl": "1.0.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-musl": "1.0.3",
+ "@rolldown/binding-openharmony-arm64": "1.0.3",
+ "@rolldown/binding-wasm32-wasi": "1.0.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.3",
+ "@rolldown/binding-win32-x64-msvc": "1.0.3"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "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.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/tsscmp": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+ "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.x"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.22.4",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
+ "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
+ "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.61.1",
+ "@typescript-eslint/parser": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1",
+ "@typescript-eslint/utils": "8.61.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.16",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
+ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.3",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..a26db48
--- /dev/null
+++ b/package.json
@@ -0,0 +1,73 @@
+{
+ "name": "teamloop",
+ "version": "1.0.0",
+ "description": "Open-source Slack bot for planning real-world team activities.",
+ "private": true,
+ "license": "MIT",
+ "homepage": "https://github.com/ayushhagarwal/teamloop#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ayushhagarwal/teamloop.git"
+ },
+ "bugs": {
+ "url": "https://github.com/ayushhagarwal/teamloop/issues"
+ },
+ "keywords": [
+ "slack",
+ "slack-bot",
+ "slack-bolt",
+ "team-activities",
+ "event-planning",
+ "rsvp",
+ "potluck",
+ "team-lunch",
+ "running",
+ "hyrox",
+ "typescript",
+ "postgresql",
+ "prisma",
+ "open-source"
+ ],
+ "type": "module",
+ "engines": {
+ "node": ">=20"
+ },
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "dev": "tsx watch src/index.ts",
+ "start": "node dist/src/index.js",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "prisma:generate": "prisma generate",
+ "prisma:migrate": "prisma migrate deploy",
+ "prisma:migrate:dev": "prisma migrate dev",
+ "prisma:seed": "tsx prisma/seed.ts"
+ },
+ "dependencies": {
+ "@prisma/client": "^6.10.1",
+ "@slack/bolt": "^4.4.0",
+ "@slack/oauth": "^3.0.5",
+ "@slack/types": "^2.21.1",
+ "@slack/web-api": "^7.17.0",
+ "dotenv": "^16.5.0",
+ "node-cron": "^4.1.1",
+ "zod": "^3.25.67"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.29.0",
+ "@types/node": "^22.15.32",
+ "@types/node-cron": "^3.0.11",
+ "eslint": "^9.29.0",
+ "prettier": "^3.5.3",
+ "prisma": "^6.10.1",
+ "tsx": "^4.20.3",
+ "typescript": "^5.8.3",
+ "typescript-eslint": "^8.34.1",
+ "vitest": "^4.1.9"
+ }
+}
diff --git a/prisma.config.ts b/prisma.config.ts
new file mode 100644
index 0000000..75046ac
--- /dev/null
+++ b/prisma.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "prisma/config";
+
+export default defineConfig({
+ schema: "prisma/schema.prisma",
+ migrations: {
+ path: "prisma/migrations",
+ seed: "tsx prisma/seed.ts",
+ },
+});
diff --git a/prisma/migrations/20260621000000_init/migration.sql b/prisma/migrations/20260621000000_init/migration.sql
new file mode 100644
index 0000000..2b2954f
--- /dev/null
+++ b/prisma/migrations/20260621000000_init/migration.sql
@@ -0,0 +1,134 @@
+-- CreateEnum
+CREATE TYPE "EventType" AS ENUM ('LUNCH', 'DINNER', 'POTLUCK', 'WEEKEND_RUN', 'MARATHON', 'HYROX');
+CREATE TYPE "EventStatus" AS ENUM ('OPEN', 'RSVP_CLOSED', 'CANCELLED', 'COMPLETED');
+CREATE TYPE "RsvpStatus" AS ENUM ('GOING', 'MAYBE', 'CANNOT_MAKE_IT');
+CREATE TYPE "ReminderType" AS ENUM ('RSVP_DEADLINE', 'EVENT_START', 'MANUAL');
+CREATE TYPE "ReminderStatus" AS ENUM ('PENDING', 'PROCESSING', 'SENT', 'FAILED', 'CANCELLED');
+
+CREATE TABLE "Workspace" (
+ "id" TEXT NOT NULL,
+ "slackTeamId" TEXT NOT NULL,
+ "name" TEXT,
+ "botUserId" TEXT,
+ "installedBySlackUserId" TEXT,
+ "accessToken" TEXT,
+ "uninstalledAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ CONSTRAINT "Workspace_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE "Channel" (
+ "id" TEXT NOT NULL,
+ "slackChannelId" TEXT NOT NULL,
+ "workspaceId" TEXT NOT NULL,
+ "name" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ CONSTRAINT "Channel_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE "Event" (
+ "id" TEXT NOT NULL,
+ "workspaceId" TEXT NOT NULL,
+ "channelId" TEXT NOT NULL,
+ "organizerSlackUserId" TEXT NOT NULL,
+ "type" "EventType" NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "location" TEXT,
+ "eventStartsAt" TIMESTAMP(3) NOT NULL,
+ "rsvpDeadlineAt" TIMESTAMP(3),
+ "status" "EventStatus" NOT NULL DEFAULT 'OPEN',
+ "slackMessageTs" TEXT,
+ "slackChannelId" TEXT NOT NULL,
+ "metadata" JSONB NOT NULL DEFAULT '{}',
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "cancelledAt" TIMESTAMP(3),
+ "closedAt" TIMESTAMP(3),
+ CONSTRAINT "Event_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE "RSVP" (
+ "id" TEXT NOT NULL,
+ "eventId" TEXT NOT NULL,
+ "slackUserId" TEXT NOT NULL,
+ "status" "RsvpStatus" NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ CONSTRAINT "RSVP_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE "Preference" (
+ "id" TEXT NOT NULL,
+ "eventId" TEXT NOT NULL,
+ "slackUserId" TEXT NOT NULL,
+ "key" TEXT NOT NULL,
+ "value" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ CONSTRAINT "Preference_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE "PotluckItem" (
+ "id" TEXT NOT NULL,
+ "eventId" TEXT NOT NULL,
+ "category" TEXT NOT NULL,
+ "itemName" TEXT NOT NULL,
+ "claimedBySlackUserId" TEXT,
+ "servesCount" INTEGER,
+ "dietaryType" TEXT,
+ "notes" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ CONSTRAINT "PotluckItem_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE "Reminder" (
+ "id" TEXT NOT NULL,
+ "eventId" TEXT NOT NULL,
+ "type" "ReminderType" NOT NULL,
+ "scheduledFor" TIMESTAMP(3) NOT NULL,
+ "sentAt" TIMESTAMP(3),
+ "status" "ReminderStatus" NOT NULL DEFAULT 'PENDING',
+ "attemptCount" INTEGER NOT NULL DEFAULT 0,
+ "lastError" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ CONSTRAINT "Reminder_pkey" PRIMARY KEY ("id")
+);
+
+CREATE TABLE "AuditLog" (
+ "id" TEXT NOT NULL,
+ "workspaceId" TEXT NOT NULL,
+ "eventId" TEXT,
+ "actorSlackUserId" TEXT,
+ "action" TEXT NOT NULL,
+ "metadata" JSONB NOT NULL DEFAULT '{}',
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
+);
+
+CREATE UNIQUE INDEX "Workspace_slackTeamId_key" ON "Workspace"("slackTeamId");
+CREATE UNIQUE INDEX "Channel_workspaceId_slackChannelId_key" ON "Channel"("workspaceId", "slackChannelId");
+CREATE INDEX "Event_workspaceId_status_idx" ON "Event"("workspaceId", "status");
+CREATE INDEX "Event_eventStartsAt_idx" ON "Event"("eventStartsAt");
+CREATE UNIQUE INDEX "RSVP_eventId_slackUserId_key" ON "RSVP"("eventId", "slackUserId");
+CREATE INDEX "RSVP_eventId_status_idx" ON "RSVP"("eventId", "status");
+CREATE UNIQUE INDEX "Preference_eventId_slackUserId_key_key" ON "Preference"("eventId", "slackUserId", "key");
+CREATE INDEX "PotluckItem_eventId_category_idx" ON "PotluckItem"("eventId", "category");
+CREATE INDEX "Reminder_status_scheduledFor_idx" ON "Reminder"("status", "scheduledFor");
+CREATE UNIQUE INDEX "Reminder_eventId_type_scheduledFor_key" ON "Reminder"("eventId", "type", "scheduledFor");
+CREATE INDEX "AuditLog_workspaceId_createdAt_idx" ON "AuditLog"("workspaceId", "createdAt");
+CREATE INDEX "AuditLog_eventId_idx" ON "AuditLog"("eventId");
+
+ALTER TABLE "Channel" ADD CONSTRAINT "Channel_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE "Event" ADD CONSTRAINT "Event_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE "Event" ADD CONSTRAINT "Event_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE "RSVP" ADD CONSTRAINT "RSVP_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE "Preference" ADD CONSTRAINT "Preference_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE "PotluckItem" ADD CONSTRAINT "PotluckItem_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE "Reminder" ADD CONSTRAINT "Reminder_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..256aa97
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,177 @@
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+enum EventType {
+ LUNCH
+ DINNER
+ POTLUCK
+ WEEKEND_RUN
+ MARATHON
+ HYROX
+}
+
+enum EventStatus {
+ OPEN
+ RSVP_CLOSED
+ CANCELLED
+ COMPLETED
+}
+
+enum RsvpStatus {
+ GOING
+ MAYBE
+ CANNOT_MAKE_IT
+}
+
+enum ReminderType {
+ RSVP_DEADLINE
+ EVENT_START
+ MANUAL
+}
+
+enum ReminderStatus {
+ PENDING
+ PROCESSING
+ SENT
+ FAILED
+ CANCELLED
+}
+
+model Workspace {
+ id String @id @default(cuid())
+ slackTeamId String @unique
+ name String?
+ botUserId String?
+ installedBySlackUserId String?
+ accessToken String?
+ uninstalledAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ channels Channel[]
+ events Event[]
+ auditLogs AuditLog[]
+}
+
+model Channel {
+ id String @id @default(cuid())
+ slackChannelId String
+ workspaceId String
+ name String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
+ events Event[]
+
+ @@unique([workspaceId, slackChannelId])
+}
+
+model Event {
+ id String @id @default(cuid())
+ workspaceId String
+ channelId String
+ organizerSlackUserId String
+ type EventType
+ title String
+ description String?
+ location String?
+ eventStartsAt DateTime
+ rsvpDeadlineAt DateTime?
+ status EventStatus @default(OPEN)
+ slackMessageTs String?
+ slackChannelId String
+ metadata Json @default("{}")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ cancelledAt DateTime?
+ closedAt DateTime?
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
+ channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
+ rsvps RSVP[]
+ preferences Preference[]
+ potluckItems PotluckItem[]
+ reminders Reminder[]
+ auditLogs AuditLog[]
+
+ @@index([workspaceId, status])
+ @@index([eventStartsAt])
+}
+
+model RSVP {
+ id String @id @default(cuid())
+ eventId String
+ slackUserId String
+ status RsvpStatus
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
+
+ @@unique([eventId, slackUserId])
+ @@index([eventId, status])
+}
+
+model Preference {
+ id String @id @default(cuid())
+ eventId String
+ slackUserId String
+ key String
+ value String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
+
+ @@unique([eventId, slackUserId, key])
+}
+
+model PotluckItem {
+ id String @id @default(cuid())
+ eventId String
+ category String
+ itemName String
+ claimedBySlackUserId String?
+ servesCount Int?
+ dietaryType String?
+ notes String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
+
+ @@index([eventId, category])
+}
+
+model Reminder {
+ id String @id @default(cuid())
+ eventId String
+ type ReminderType
+ scheduledFor DateTime
+ sentAt DateTime?
+ status ReminderStatus @default(PENDING)
+ attemptCount Int @default(0)
+ lastError String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
+
+ @@index([status, scheduledFor])
+ @@unique([eventId, type, scheduledFor])
+}
+
+model AuditLog {
+ id String @id @default(cuid())
+ workspaceId String
+ eventId String?
+ actorSlackUserId String?
+ action String
+ metadata Json @default("{}")
+ createdAt DateTime @default(now())
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
+ event Event? @relation(fields: [eventId], references: [id], onDelete: SetNull)
+
+ @@index([workspaceId, createdAt])
+ @@index([eventId])
+}
diff --git a/prisma/seed.ts b/prisma/seed.ts
new file mode 100644
index 0000000..e23721b
--- /dev/null
+++ b/prisma/seed.ts
@@ -0,0 +1,20 @@
+import { prisma } from "../src/db/prisma.js";
+
+async function main(): Promise {
+ await prisma.workspace.upsert({
+ where: { slackTeamId: "T_TEAMLOOP_DEMO" },
+ update: {},
+ create: {
+ slackTeamId: "T_TEAMLOOP_DEMO",
+ name: "TeamLoop Demo",
+ },
+ });
+}
+
+main()
+ .then(async () => prisma.$disconnect())
+ .catch(async (error: unknown) => {
+ console.error(error);
+ await prisma.$disconnect();
+ process.exitCode = 1;
+ });
diff --git a/screenshots/README.md b/screenshots/README.md
new file mode 100644
index 0000000..ab0d7f0
--- /dev/null
+++ b/screenshots/README.md
@@ -0,0 +1,4 @@
+# Screenshots
+
+Add sanitized Slack screenshots here before publishing a release. Never include
+real workspace names, private channels, tokens, or participant details.
diff --git a/src/app.ts b/src/app.ts
new file mode 100644
index 0000000..1b827d1
--- /dev/null
+++ b/src/app.ts
@@ -0,0 +1,100 @@
+import { App, ExpressReceiver } from "@slack/bolt";
+import type { ScheduledTask } from "node-cron";
+import { registerDinnerCommand } from "./commands/dinner.command.js";
+import { registerHyroxCommand } from "./commands/hyrox.command.js";
+import { registerLunchCommand } from "./commands/lunch.command.js";
+import { registerMarathonCommand } from "./commands/marathon.command.js";
+import { registerPotluckCommand } from "./commands/potluck.command.js";
+import { registerWeekendRunCommand } from "./commands/weekendrun.command.js";
+import type { Env } from "./config/env.js";
+import { Logger } from "./config/logger.js";
+import { SLACK_ENDPOINTS, SLACK_SCOPES } from "./config/slack.js";
+import { prisma } from "./db/prisma.js";
+import { registerEventSubmissionHandlers } from "./interactions/event-submissions.js";
+import { registerOrganizerActions } from "./interactions/organizer.actions.js";
+import { registerPotluckActions } from "./interactions/potluck.actions.js";
+import { registerPreferenceActions } from "./interactions/preference.actions.js";
+import { registerRsvpActions } from "./interactions/rsvp.actions.js";
+import { startReminderJob } from "./jobs/reminder.job.js";
+import { EventService } from "./services/event.service.js";
+import { PrismaInstallationStore } from "./services/oauth-installation.store.js";
+import { PotluckService } from "./services/potluck.service.js";
+import { PreferenceService } from "./services/preference.service.js";
+import { ReminderService } from "./services/reminder.service.js";
+import { RsvpService } from "./services/rsvp.service.js";
+import { SlackMessageService } from "./services/slack-message.service.js";
+import { WorkspaceService } from "./services/workspace.service.js";
+
+export interface TeamLoopRuntime {
+ app: App;
+ receiver: ExpressReceiver;
+ logger: Logger;
+ startJobs: () => ScheduledTask;
+}
+
+export function createTeamLoopApp(env: Env): TeamLoopRuntime {
+ const logger = new Logger(env.LOG_LEVEL);
+ const workspaces = new WorkspaceService(prisma, env.TOKEN_ENCRYPTION_KEY);
+ const receiver =
+ env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET && env.SLACK_STATE_SECRET
+ ? new ExpressReceiver({
+ signingSecret: env.SLACK_SIGNING_SECRET,
+ endpoints: SLACK_ENDPOINTS.events,
+ clientId: env.SLACK_CLIENT_ID,
+ clientSecret: env.SLACK_CLIENT_SECRET,
+ stateSecret: env.SLACK_STATE_SECRET,
+ redirectUri: `${env.APP_BASE_URL}${SLACK_ENDPOINTS.oauthRedirect}`,
+ scopes: [...SLACK_SCOPES],
+ installationStore: new PrismaInstallationStore(workspaces),
+ installerOptions: {
+ installPath: SLACK_ENDPOINTS.install,
+ redirectUriPath: SLACK_ENDPOINTS.oauthRedirect,
+ },
+ })
+ : new ExpressReceiver({
+ signingSecret: env.SLACK_SIGNING_SECRET,
+ endpoints: SLACK_ENDPOINTS.events,
+ });
+ receiver.app.get("/health", (_request, response) => {
+ response.status(200).json({ ok: true, service: "teamloop" });
+ });
+
+ const app = new App({
+ receiver,
+ ...(env.SLACK_BOT_TOKEN ? { token: env.SLACK_BOT_TOKEN } : {}),
+ });
+ const events = new EventService(prisma);
+ const rsvps = new RsvpService(prisma);
+ const preferences = new PreferenceService(prisma);
+ const potluck = new PotluckService(prisma);
+ const reminders = new ReminderService(prisma);
+ const messages = new SlackMessageService();
+
+ registerLunchCommand(app);
+ registerDinnerCommand(app);
+ registerPotluckCommand(app);
+ registerWeekendRunCommand(app);
+ registerMarathonCommand(app);
+ registerHyroxCommand(app);
+ registerEventSubmissionHandlers(app, events, reminders, messages);
+ registerRsvpActions(app, rsvps, events, messages);
+ registerPreferenceActions(app, events, preferences, messages);
+ registerPotluckActions(app, events, potluck, messages);
+ registerOrganizerActions(app, events, reminders, messages);
+
+ app.event("app_uninstalled", async ({ body }) => {
+ const teamId = "team_id" in body ? body.team_id : undefined;
+ if (typeof teamId === "string") await workspaces.markUninstalled(teamId);
+ });
+
+ app.error(async (error) => {
+ logger.error("Unhandled Slack listener error", { error: error.message });
+ });
+
+ return {
+ app,
+ receiver,
+ logger,
+ startJobs: () => startReminderJob(reminders, workspaces, env, logger),
+ };
+}
diff --git a/src/blocks/event-card.blocks.ts b/src/blocks/event-card.blocks.ts
new file mode 100644
index 0000000..0d32a26
--- /dev/null
+++ b/src/blocks/event-card.blocks.ts
@@ -0,0 +1,311 @@
+import { EventStatus, EventType, RsvpStatus, type Preference } from "@prisma/client";
+import type { KnownBlock } from "@slack/types";
+import type { EventMetadata, EventWithRelations } from "../types/event.types.js";
+import { slackDate } from "../utils/date.js";
+import { escapeMrkdwn, titleCase, truncate } from "../utils/text.js";
+import { button, contextBlock, divider, mrkdwnSection } from "./shared.blocks.js";
+
+const activity: Record<
+ EventType,
+ { emoji: string; label: string; preferenceLabel: string; preferenceAction: string }
+> = {
+ LUNCH: {
+ emoji: "🍱",
+ label: "Lunch",
+ preferenceLabel: "Set food preference",
+ preferenceAction: "open_preference",
+ },
+ DINNER: {
+ emoji: "🍽️",
+ label: "Dinner",
+ preferenceLabel: "Set food preference",
+ preferenceAction: "open_preference",
+ },
+ POTLUCK: {
+ emoji: "🥘",
+ label: "Potluck",
+ preferenceLabel: "Bring something",
+ preferenceAction: "open_potluck_item",
+ },
+ WEEKEND_RUN: {
+ emoji: "🏃",
+ label: "Weekend run",
+ preferenceLabel: "Set pace preference",
+ preferenceAction: "open_preference",
+ },
+ MARATHON: {
+ emoji: "🏅",
+ label: "Marathon",
+ preferenceLabel: "Set running preference",
+ preferenceAction: "open_preference",
+ },
+ HYROX: {
+ emoji: "🔥",
+ label: "HYROX",
+ preferenceLabel: "Set HYROX preference",
+ preferenceAction: "open_preference",
+ },
+};
+
+export function renderEventCard(event: EventWithRelations): KnownBlock[] {
+ const definition = activity[event.type];
+ const metadata = readMetadata(event.metadata);
+ const counts = countRsvps(event);
+ const value = JSON.stringify({ eventId: event.id });
+ const blocks: KnownBlock[] = [
+ {
+ type: "header",
+ text: {
+ type: "plain_text",
+ text: `${definition.emoji} ${truncate(event.title, 140)}`,
+ emoji: true,
+ },
+ },
+ contextBlock([
+ `Created by <@${event.organizerSlackUserId}>`,
+ `${definition.label} · *Status: ${statusLabel(event.status)}*`,
+ ]),
+ {
+ type: "section",
+ fields: [
+ { type: "mrkdwn", text: `*📅 Date & time*\n${slackDate(event.eventStartsAt)}` },
+ {
+ type: "mrkdwn",
+ text: `*📍 Location*\n${escapeMrkdwn(event.location ?? "To be confirmed")}`,
+ },
+ {
+ type: "mrkdwn",
+ text: `*⏰ RSVP by*\n${
+ event.rsvpDeadlineAt ? slackDate(event.rsvpDeadlineAt) : "No deadline"
+ }`,
+ },
+ {
+ type: "mrkdwn",
+ text: `*Who’s joining?*\n✅ ${counts.going} · 🤔 ${counts.maybe} · ❌ ${counts.cannot}`,
+ },
+ ],
+ },
+ ];
+
+ const detail = activityDetail(event.type, metadata, event.preferences, event);
+ if (detail) blocks.push(mrkdwnSection(detail));
+ if (event.description) {
+ blocks.push(
+ mrkdwnSection(`*💬 Notes*\n${escapeMrkdwn(truncate(event.description, 700))}`),
+ );
+ }
+
+ if (event.status === EventStatus.CANCELLED) {
+ blocks.push(divider(), mrkdwnSection("🚫 *This event has been cancelled.*"), {
+ type: "actions",
+ elements: [button("View summary", "view_summary", value)],
+ });
+ return blocks;
+ }
+
+ if (event.status === EventStatus.RSVP_CLOSED) {
+ blocks.push(divider(), mrkdwnSection("🔒 *RSVPs are closed for this event.*"), {
+ type: "actions",
+ elements: [button("View summary", "view_summary", value)],
+ });
+ } else {
+ blocks.push({
+ type: "actions",
+ block_id: `rsvp_${event.id}`,
+ elements: [
+ button("✅ Going", "rsvp_going", value, "primary"),
+ button("🤔 Maybe", "rsvp_maybe", value),
+ button("❌ Can’t make it", "rsvp_cannot", value),
+ ...(showsPreferenceAction(event.type, metadata)
+ ? [
+ button(
+ preferenceActionLabel(event.type, metadata, definition.preferenceLabel),
+ definition.preferenceAction,
+ value,
+ ),
+ ]
+ : []),
+ button("View summary", "view_summary", value),
+ ],
+ });
+ }
+
+ blocks.push(divider(), contextBlock(["Organizer controls"]), {
+ type: "actions",
+ block_id: `organizer_${event.id}`,
+ elements: [
+ button("Edit", "edit_event", value),
+ button("Send reminder", "send_reminder", value),
+ ...(event.status === EventStatus.OPEN
+ ? [button("Close RSVP", "close_rsvp", value)]
+ : []),
+ button("Cancel", "cancel_event", value, "danger", {
+ title: "Cancel event?",
+ text: "The event card will remain visible and pending reminders will stop.",
+ confirm: "Cancel event",
+ deny: "Keep event",
+ }),
+ ],
+ });
+ return blocks;
+}
+
+function countRsvps(event: EventWithRelations) {
+ return {
+ going: event.rsvps.filter((rsvp) => rsvp.status === RsvpStatus.GOING).length,
+ maybe: event.rsvps.filter((rsvp) => rsvp.status === RsvpStatus.MAYBE).length,
+ cannot: event.rsvps.filter((rsvp) => rsvp.status === RsvpStatus.CANNOT_MAKE_IT)
+ .length,
+ };
+}
+
+function statusLabel(status: EventStatus): string {
+ if (status === EventStatus.RSVP_CLOSED) return "RSVP closed";
+ return titleCase(status);
+}
+
+function readMetadata(value: unknown): EventMetadata {
+ if (value && typeof value === "object" && !Array.isArray(value)) {
+ return value as EventMetadata;
+ }
+ return {};
+}
+
+function activityDetail(
+ type: EventType,
+ metadata: EventMetadata,
+ preferences: Preference[],
+ event: EventWithRelations,
+): string {
+ const preferenceSummary = summarizePreferences(preferences);
+ switch (type) {
+ case EventType.LUNCH:
+ return compactLines([
+ `*🍴 Lunch details*`,
+ metadata.budget ? `Budget: ${escapeMrkdwn(String(metadata.budget))}` : "",
+ preferenceSummary,
+ ]);
+ case EventType.DINNER:
+ return compactLines([
+ `*🍴 Dinner details*`,
+ metadata.budget ? `Budget: ${escapeMrkdwn(String(metadata.budget))}` : "",
+ `Plus-one: ${metadata.plusOneAllowed ? "Allowed" : "Not enabled"}`,
+ preferenceSummary,
+ ]);
+ case EventType.POTLUCK: {
+ const claimed = event.potluckItems.slice(0, 5).map((item) => {
+ const by = item.claimedBySlackUserId
+ ? ` by <@${item.claimedBySlackUserId}>`
+ : "";
+ const detail = [
+ item.dietaryType,
+ item.servesCount ? `serves ${item.servesCount}` : "",
+ ]
+ .filter(Boolean)
+ .join(" · ");
+ return `• *${escapeMrkdwn(item.category)}:* ${escapeMrkdwn(item.itemName)}${by}${detail ? ` _(${escapeMrkdwn(detail)})_` : ""}`;
+ });
+ const needed = Array.isArray(metadata.neededCategories)
+ ? metadata.neededCategories.filter(
+ (category) =>
+ !event.potluckItems.some((item) => item.category === category),
+ )
+ : [];
+ return compactLines([
+ "*🥘 Potluck plan*",
+ claimed.length ? `*Claimed*\n${claimed.join("\n")}` : "No items claimed yet.",
+ needed.length
+ ? `*Still needed:* ${needed.map(escapeMrkdwn).join(", ")}`
+ : "All requested categories have a contribution.",
+ event.potluckItems.length > 5
+ ? "_More items are available in the summary._"
+ : "",
+ ]);
+ }
+ case EventType.WEEKEND_RUN:
+ return compactLines([
+ "*🏃 Run details*",
+ `Distance: ${display(metadata.distance, "Flexible")} · Pace: ${display(metadata.pace, "Mixed groups")}`,
+ `Beginner-friendly: ${yesNo(metadata.beginnerFriendly)}`,
+ metadata.routeLink ? `Route: ${display(metadata.routeLink, "")}` : "",
+ preferenceSummary,
+ ]);
+ case EventType.MARATHON:
+ return compactLines([
+ "*🏅 Marathon plan*",
+ metadata.raceName ? `Race: ${escapeMrkdwn(String(metadata.raceName))}` : "",
+ `Distance: ${display(metadata.distance, "Flexible")}${
+ metadata.targetPace
+ ? ` · Target pace: ${display(metadata.targetPace, "")}`
+ : ""
+ }`,
+ `Beginner group: ${yesNo(metadata.beginnerFriendly)}`,
+ preferenceSummary,
+ ]);
+ case EventType.HYROX:
+ return compactLines([
+ "*🔥 HYROX details*",
+ `Workout: ${display(metadata.workoutType, "Full prep")} · Intensity: ${display(metadata.intensity, "Mixed")}`,
+ `Partner needed: ${yesNo(metadata.partnerNeeded)} · Beginner-friendly: ${yesNo(metadata.beginnerFriendly)}`,
+ preferenceSummary,
+ ]);
+ }
+}
+
+function summarizePreferences(preferences: Preference[]): string {
+ if (!preferences.length) return "";
+ const counts = new Map();
+ for (const preference of preferences) {
+ const label =
+ preference.key === "plusOne"
+ ? `Plus-one ${preference.value}`
+ : preference.key === "targetPace"
+ ? `Pace ${preference.value}`
+ : preference.value;
+ counts.set(label, (counts.get(label) ?? 0) + 1);
+ }
+ const summary = [...counts.entries()]
+ .slice(0, 4)
+ .map(([value, count]) => `${escapeMrkdwn(value)} ${count}`)
+ .join(" · ");
+ return summary ? `*Preferences:* ${summary}` : "";
+}
+
+function compactLines(lines: string[]): string {
+ return lines.filter(Boolean).join("\n");
+}
+
+function yesNo(value: EventMetadata[string] | undefined): string {
+ return value === true || value === "Yes" ? "Yes" : "No";
+}
+
+function display(value: EventMetadata[string] | undefined, fallback: string): string {
+ if (value === undefined || value === null || value === "") return fallback;
+ if (Array.isArray(value)) return value.join(", ");
+ return String(value);
+}
+
+function showsPreferenceAction(type: EventType, metadata: EventMetadata): boolean {
+ if (type === EventType.LUNCH) return metadata.foodPreferencesEnabled !== false;
+ if (type === EventType.DINNER) {
+ return (
+ metadata.foodPreferencesEnabled !== false || metadata.plusOneAllowed === true
+ );
+ }
+ return true;
+}
+
+function preferenceActionLabel(
+ type: EventType,
+ metadata: EventMetadata,
+ fallback: string,
+): string {
+ if (
+ type === EventType.DINNER &&
+ metadata.foodPreferencesEnabled === false &&
+ metadata.plusOneAllowed === true
+ ) {
+ return "Set plus-one";
+ }
+ return fallback;
+}
diff --git a/src/blocks/shared.blocks.ts b/src/blocks/shared.blocks.ts
new file mode 100644
index 0000000..98b6813
--- /dev/null
+++ b/src/blocks/shared.blocks.ts
@@ -0,0 +1,50 @@
+import type { KnownBlock } from "@slack/types";
+
+export function divider(): KnownBlock {
+ return { type: "divider" };
+}
+
+export function mrkdwnSection(text: string): KnownBlock {
+ return {
+ type: "section",
+ text: { type: "mrkdwn", text },
+ };
+}
+
+export function contextBlock(elements: string[]): KnownBlock {
+ return {
+ type: "context",
+ elements: elements.map((text) => ({ type: "mrkdwn", text })),
+ };
+}
+
+export function button(
+ text: string,
+ actionId: string,
+ value: string,
+ style?: "primary" | "danger",
+ confirm?: {
+ title: string;
+ text: string;
+ confirm: string;
+ deny: string;
+ },
+) {
+ return {
+ type: "button" as const,
+ text: { type: "plain_text" as const, text, emoji: true },
+ action_id: actionId,
+ value,
+ ...(style ? { style } : {}),
+ ...(confirm
+ ? {
+ confirm: {
+ title: { type: "plain_text" as const, text: confirm.title },
+ text: { type: "mrkdwn" as const, text: confirm.text },
+ confirm: { type: "plain_text" as const, text: confirm.confirm },
+ deny: { type: "plain_text" as const, text: confirm.deny },
+ },
+ }
+ : {}),
+ };
+}
diff --git a/src/blocks/summary.blocks.ts b/src/blocks/summary.blocks.ts
new file mode 100644
index 0000000..87cf133
--- /dev/null
+++ b/src/blocks/summary.blocks.ts
@@ -0,0 +1,50 @@
+import { RsvpStatus } from "@prisma/client";
+import type { KnownBlock } from "@slack/types";
+import type { EventWithRelations } from "../types/event.types.js";
+import { escapeMrkdwn } from "../utils/text.js";
+import { divider, mrkdwnSection } from "./shared.blocks.js";
+
+export function renderSummary(event: EventWithRelations): KnownBlock[] {
+ const byStatus = (status: RsvpStatus) =>
+ event.rsvps
+ .filter((rsvp) => rsvp.status === status)
+ .map((rsvp) => `<@${rsvp.slackUserId}>`);
+ const going = byStatus(RsvpStatus.GOING);
+ const maybe = byStatus(RsvpStatus.MAYBE);
+ const cannot = byStatus(RsvpStatus.CANNOT_MAKE_IT);
+
+ const preferenceRows = event.preferences.map(
+ (preference) =>
+ `• <@${preference.slackUserId}> — ${
+ preference.key === "primary"
+ ? escapeMrkdwn(preference.value)
+ : `${escapeMrkdwn(preference.key)}: ${escapeMrkdwn(preference.value)}`
+ }`,
+ );
+ const potluckRows = event.potluckItems.map((item) => {
+ const owner = item.claimedBySlackUserId
+ ? `<@${item.claimedBySlackUserId}>`
+ : "Unclaimed";
+ return `• *${escapeMrkdwn(item.category)}:* ${escapeMrkdwn(item.itemName)} — ${owner}`;
+ });
+
+ return [
+ {
+ type: "header",
+ text: { type: "plain_text", text: `Event summary · ${event.title}`, emoji: true },
+ },
+ mrkdwnSection(
+ [
+ `*✅ Going (${going.length})*\n${going.join(", ") || "No responses"}`,
+ `*🤔 Maybe (${maybe.length})*\n${maybe.join(", ") || "No responses"}`,
+ `*❌ Can’t make it (${cannot.length})*\n${cannot.join(", ") || "No responses"}`,
+ ].join("\n\n"),
+ ),
+ ...(preferenceRows.length
+ ? [divider(), mrkdwnSection(`*Preferences*\n${preferenceRows.join("\n")}`)]
+ : []),
+ ...(potluckRows.length
+ ? [divider(), mrkdwnSection(`*Potluck items*\n${potluckRows.join("\n")}`)]
+ : []),
+ ];
+}
diff --git a/src/commands/dinner.command.ts b/src/commands/dinner.command.ts
new file mode 100644
index 0000000..9ddc024
--- /dev/null
+++ b/src/commands/dinner.command.ts
@@ -0,0 +1,7 @@
+import { EventType } from "@prisma/client";
+import type { App } from "@slack/bolt";
+import { buildDinnerModal } from "../modals/dinner.modal.js";
+import { registerActivityCommand } from "./shared.command.js";
+
+export const registerDinnerCommand = (app: App): void =>
+ registerActivityCommand(app, "/dinner", EventType.DINNER, buildDinnerModal);
diff --git a/src/commands/hyrox.command.ts b/src/commands/hyrox.command.ts
new file mode 100644
index 0000000..14210bc
--- /dev/null
+++ b/src/commands/hyrox.command.ts
@@ -0,0 +1,7 @@
+import { EventType } from "@prisma/client";
+import type { App } from "@slack/bolt";
+import { buildHyroxModal } from "../modals/hyrox.modal.js";
+import { registerActivityCommand } from "./shared.command.js";
+
+export const registerHyroxCommand = (app: App): void =>
+ registerActivityCommand(app, "/hyrox", EventType.HYROX, buildHyroxModal);
diff --git a/src/commands/lunch.command.ts b/src/commands/lunch.command.ts
new file mode 100644
index 0000000..d51c4a0
--- /dev/null
+++ b/src/commands/lunch.command.ts
@@ -0,0 +1,7 @@
+import { EventType } from "@prisma/client";
+import type { App } from "@slack/bolt";
+import { buildLunchModal } from "../modals/lunch.modal.js";
+import { registerActivityCommand } from "./shared.command.js";
+
+export const registerLunchCommand = (app: App): void =>
+ registerActivityCommand(app, "/lunch", EventType.LUNCH, buildLunchModal);
diff --git a/src/commands/marathon.command.ts b/src/commands/marathon.command.ts
new file mode 100644
index 0000000..98c1b12
--- /dev/null
+++ b/src/commands/marathon.command.ts
@@ -0,0 +1,7 @@
+import { EventType } from "@prisma/client";
+import type { App } from "@slack/bolt";
+import { buildMarathonModal } from "../modals/marathon.modal.js";
+import { registerActivityCommand } from "./shared.command.js";
+
+export const registerMarathonCommand = (app: App): void =>
+ registerActivityCommand(app, "/marathon", EventType.MARATHON, buildMarathonModal);
diff --git a/src/commands/potluck.command.ts b/src/commands/potluck.command.ts
new file mode 100644
index 0000000..6a345ee
--- /dev/null
+++ b/src/commands/potluck.command.ts
@@ -0,0 +1,7 @@
+import { EventType } from "@prisma/client";
+import type { App } from "@slack/bolt";
+import { buildPotluckModal } from "../modals/potluck.modal.js";
+import { registerActivityCommand } from "./shared.command.js";
+
+export const registerPotluckCommand = (app: App): void =>
+ registerActivityCommand(app, "/potluck", EventType.POTLUCK, buildPotluckModal);
diff --git a/src/commands/shared.command.ts b/src/commands/shared.command.ts
new file mode 100644
index 0000000..0033891
--- /dev/null
+++ b/src/commands/shared.command.ts
@@ -0,0 +1,29 @@
+import type { App } from "@slack/bolt";
+import type { EventType } from "@prisma/client";
+import type { ModalView } from "@slack/types";
+import type { CommandMetadata } from "../types/slack.types.js";
+
+type ModalBuilder = (metadata: CommandMetadata) => ModalView;
+
+export function registerActivityCommand(
+ app: App,
+ command: string,
+ eventType: EventType,
+ buildModal: ModalBuilder,
+): void {
+ app.command(command, async ({ ack, body, client }) => {
+ await ack();
+ const metadata: CommandMetadata = {
+ channelId: body.channel_id,
+ channelName: body.channel_name,
+ userId: body.user_id,
+ teamId: body.team_id,
+ teamName: body.team_domain,
+ eventType,
+ };
+ await client.views.open({
+ trigger_id: body.trigger_id,
+ view: buildModal(metadata),
+ });
+ });
+}
diff --git a/src/commands/weekendrun.command.ts b/src/commands/weekendrun.command.ts
new file mode 100644
index 0000000..77731a9
--- /dev/null
+++ b/src/commands/weekendrun.command.ts
@@ -0,0 +1,12 @@
+import { EventType } from "@prisma/client";
+import type { App } from "@slack/bolt";
+import { buildWeekendRunModal } from "../modals/weekendrun.modal.js";
+import { registerActivityCommand } from "./shared.command.js";
+
+export const registerWeekendRunCommand = (app: App): void =>
+ registerActivityCommand(
+ app,
+ "/weekendrun",
+ EventType.WEEKEND_RUN,
+ buildWeekendRunModal,
+ );
diff --git a/src/config/env.ts b/src/config/env.ts
new file mode 100644
index 0000000..34e4d0f
--- /dev/null
+++ b/src/config/env.ts
@@ -0,0 +1,84 @@
+import "dotenv/config";
+import { z } from "zod";
+
+const optionalSecret = z.preprocess(
+ (value) => (typeof value === "string" && value.trim() === "" ? undefined : value),
+ z.string().trim().min(1).optional(),
+);
+
+const envSchema = z
+ .object({
+ NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
+ PORT: z.coerce.number().int().positive().default(3000),
+ LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
+ DATABASE_URL: z.string().min(1),
+ APP_BASE_URL: z.string().url().default("http://localhost:3000"),
+ DEFAULT_TIMEZONE: z.string().default("UTC"),
+ SLACK_SIGNING_SECRET: z.string().min(1),
+ SLACK_BOT_TOKEN: optionalSecret,
+ SLACK_CLIENT_ID: optionalSecret,
+ SLACK_CLIENT_SECRET: optionalSecret,
+ SLACK_STATE_SECRET: optionalSecret,
+ TOKEN_ENCRYPTION_KEY: optionalSecret,
+ })
+ .superRefine((value, context) => {
+ if (
+ !value.SLACK_BOT_TOKEN &&
+ (!value.SLACK_CLIENT_ID || !value.SLACK_CLIENT_SECRET)
+ ) {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["SLACK_BOT_TOKEN"],
+ message:
+ "Set SLACK_BOT_TOKEN for local development or configure SLACK_CLIENT_ID and SLACK_CLIENT_SECRET for OAuth.",
+ });
+ }
+
+ if (
+ value.SLACK_CLIENT_ID &&
+ value.SLACK_CLIENT_SECRET &&
+ (!value.SLACK_STATE_SECRET || value.SLACK_STATE_SECRET.length < 32)
+ ) {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["SLACK_STATE_SECRET"],
+ message: "SLACK_STATE_SECRET must be at least 32 characters for OAuth.",
+ });
+ }
+ if (value.SLACK_BOT_TOKEN && value.SLACK_CLIENT_ID) {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["SLACK_BOT_TOKEN"],
+ message: "Use either local bot-token mode or OAuth mode, not both.",
+ });
+ }
+ if (value.SLACK_CLIENT_ID && !isEncryptionKey(value.TOKEN_ENCRYPTION_KEY)) {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["TOKEN_ENCRYPTION_KEY"],
+ message: "OAuth mode requires a base64-encoded 32-byte encryption key.",
+ });
+ }
+ });
+
+export type Env = z.infer;
+
+export function loadEnv(source: NodeJS.ProcessEnv = process.env): Env {
+ const result = envSchema.safeParse(source);
+ if (!result.success) {
+ const details = result.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join("\n");
+ throw new Error(`Invalid environment configuration:\n${details}`);
+ }
+ return result.data;
+}
+
+function isEncryptionKey(value: string | undefined): boolean {
+ if (!value) return false;
+ try {
+ return Buffer.from(value, "base64").length === 32;
+ } catch {
+ return false;
+ }
+}
diff --git a/src/config/logger.ts b/src/config/logger.ts
new file mode 100644
index 0000000..4b27d7f
--- /dev/null
+++ b/src/config/logger.ts
@@ -0,0 +1,52 @@
+type LogLevel = "debug" | "info" | "warn" | "error";
+type Context = Record;
+
+const priorities: Record = {
+ debug: 10,
+ info: 20,
+ warn: 30,
+ error: 40,
+};
+
+export class Logger {
+ constructor(private readonly level: LogLevel = "info") {}
+
+ debug(message: string, context: Context = {}): void {
+ this.write("debug", message, context);
+ }
+
+ info(message: string, context: Context = {}): void {
+ this.write("info", message, context);
+ }
+
+ warn(message: string, context: Context = {}): void {
+ this.write("warn", message, context);
+ }
+
+ error(message: string, context: Context = {}): void {
+ this.write("error", message, context);
+ }
+
+ private write(level: LogLevel, message: string, context: Context): void {
+ if (priorities[level] < priorities[this.level]) return;
+ const payload = JSON.stringify({
+ time: new Date().toISOString(),
+ level,
+ message,
+ ...sanitize(context),
+ });
+ if (level === "error") console.error(payload);
+ else if (level === "warn") console.warn(payload);
+ else console.log(payload);
+ }
+}
+
+function sanitize(context: Context): Context {
+ const blocked = new Set(["token", "accessToken", "clientSecret", "signingSecret"]);
+ return Object.fromEntries(
+ Object.entries(context).map(([key, value]) => [
+ key,
+ blocked.has(key) ? "[REDACTED]" : value,
+ ]),
+ );
+}
diff --git a/src/config/slack.ts b/src/config/slack.ts
new file mode 100644
index 0000000..5d1b9c3
--- /dev/null
+++ b/src/config/slack.ts
@@ -0,0 +1,7 @@
+export const SLACK_SCOPES = ["commands", "chat:write", "chat:write.public"] as const;
+
+export const SLACK_ENDPOINTS = {
+ events: "/slack/events",
+ install: "/slack/install",
+ oauthRedirect: "/slack/oauth_redirect",
+} as const;
diff --git a/src/db/prisma.ts b/src/db/prisma.ts
new file mode 100644
index 0000000..ce3a843
--- /dev/null
+++ b/src/db/prisma.ts
@@ -0,0 +1,11 @@
+import { PrismaClient } from "@prisma/client";
+
+declare global {
+ var __teamloopPrisma: PrismaClient | undefined;
+}
+
+export const prisma = globalThis.__teamloopPrisma ?? new PrismaClient();
+
+if (process.env.NODE_ENV !== "production") {
+ globalThis.__teamloopPrisma = prisma;
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..832890e
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,27 @@
+import { createTeamLoopApp } from "./app.js";
+import { loadEnv } from "./config/env.js";
+import { prisma } from "./db/prisma.js";
+
+const env = loadEnv();
+const runtime = createTeamLoopApp(env);
+const reminderJob = runtime.startJobs();
+
+await runtime.app.start(env.PORT);
+runtime.logger.info("TeamLoop is running", {
+ port: env.PORT,
+ oauth: Boolean(env.SLACK_CLIENT_ID),
+});
+
+async function shutdown(signal: string): Promise {
+ runtime.logger.info("Shutting down TeamLoop", { signal });
+ await reminderJob.stop();
+ await runtime.receiver.stop();
+ await prisma.$disconnect();
+}
+
+process.once("SIGINT", () => {
+ void shutdown("SIGINT").catch(console.error);
+});
+process.once("SIGTERM", () => {
+ void shutdown("SIGTERM").catch(console.error);
+});
diff --git a/src/interactions/event-submissions.ts b/src/interactions/event-submissions.ts
new file mode 100644
index 0000000..ce1667f
--- /dev/null
+++ b/src/interactions/event-submissions.ts
@@ -0,0 +1,171 @@
+import { EventType } from "@prisma/client";
+import type { App } from "@slack/bolt";
+import { dinnerModalConfig } from "../modals/dinner.modal.js";
+import { hyroxModalConfig } from "../modals/hyrox.modal.js";
+import { lunchModalConfig } from "../modals/lunch.modal.js";
+import { marathonModalConfig } from "../modals/marathon.modal.js";
+import { potluckModalConfig } from "../modals/potluck.modal.js";
+import {
+ extractActivityValues,
+ type ActivityModalConfig,
+} from "../modals/shared.modal.js";
+import { weekendRunModalConfig } from "../modals/weekendrun.modal.js";
+import { EventService } from "../services/event.service.js";
+import { ReminderService } from "../services/reminder.service.js";
+import { SlackMessageService } from "../services/slack-message.service.js";
+import type { EventMetadata } from "../types/event.types.js";
+import type { CommandMetadata } from "../types/slack.types.js";
+import {
+ eventInputSchema,
+ formatZodErrors,
+ type ValidatedEventInput,
+} from "../utils/validation.js";
+
+const configs: Array<{ type: EventType; config: ActivityModalConfig }> = [
+ { type: EventType.LUNCH, config: lunchModalConfig },
+ { type: EventType.DINNER, config: dinnerModalConfig },
+ { type: EventType.POTLUCK, config: potluckModalConfig },
+ { type: EventType.WEEKEND_RUN, config: weekendRunModalConfig },
+ { type: EventType.MARATHON, config: marathonModalConfig },
+ { type: EventType.HYROX, config: hyroxModalConfig },
+];
+
+export function registerEventSubmissionHandlers(
+ app: App,
+ eventService: EventService,
+ reminderService: ReminderService,
+ messages: SlackMessageService,
+): void {
+ for (const { type, config } of configs) {
+ app.view(config.callbackId, async ({ ack, body, view, client, logger }) => {
+ const raw = extractActivityValues(view.state.values, config.fields);
+ const parsed = eventInputSchema.safeParse(raw);
+ const customErrors = activityErrors(type, raw);
+ if (!parsed.success || Object.keys(customErrors).length) {
+ await ack({
+ response_action: "errors",
+ errors: {
+ ...(parsed.success ? {} : formatZodErrors(parsed.error)),
+ ...customErrors,
+ },
+ });
+ return;
+ }
+
+ await ack();
+ try {
+ const command = JSON.parse(view.private_metadata) as CommandMetadata;
+ const event = await eventService.create({
+ slackTeamId: command.teamId,
+ ...(command.teamName ? { slackTeamName: command.teamName } : {}),
+ slackChannelId: command.channelId,
+ ...(command.channelName ? { slackChannelName: command.channelName } : {}),
+ organizerSlackUserId: body.user.id,
+ type,
+ title: parsed.data.title,
+ ...(parsed.data.notes ? { description: parsed.data.notes } : {}),
+ location: parsed.data.location,
+ eventStartsAt: parsed.data.eventStartsAt,
+ rsvpDeadlineAt: parsed.data.rsvpDeadlineAt,
+ metadata: buildMetadata(type, raw),
+ });
+ const posted = await messages.postEvent(client, event);
+ if (!posted.ts) throw new Error("Slack did not return a message timestamp.");
+ await eventService.setSlackMessage(event.id, command.channelId, posted.ts);
+ try {
+ await reminderService.createDefaults({
+ eventId: event.id,
+ eventStartsAt: event.eventStartsAt,
+ ...(event.rsvpDeadlineAt ? { rsvpDeadlineAt: event.rsvpDeadlineAt } : {}),
+ ...(parsed.data.reminderMinutes !== undefined
+ ? { eventReminderMinutes: parsed.data.reminderMinutes }
+ : {}),
+ });
+ } catch (error) {
+ logger.warn("Event created, but default reminders could not be scheduled", {
+ eventId: event.id,
+ error,
+ });
+ }
+ } catch (error) {
+ logger.error("Failed to create TeamLoop event", error);
+ const metadata = JSON.parse(view.private_metadata) as CommandMetadata;
+ await messages.ephemeral(
+ client,
+ metadata.channelId,
+ body.user.id,
+ "Something went wrong while creating the event. Please try again.",
+ );
+ }
+ });
+ }
+}
+
+function activityErrors(
+ type: EventType,
+ raw: Record,
+): Record {
+ if (
+ (type === EventType.WEEKEND_RUN || type === EventType.MARATHON) &&
+ raw.distance === "Custom" &&
+ !raw.customDistance?.trim()
+ ) {
+ return { customDistance: "Enter a custom distance." };
+ }
+ return {};
+}
+
+function buildMetadata(type: EventType, raw: Record): EventMetadata {
+ const value = (key: string) => raw[key]?.trim() ?? "";
+ const yes = (key: string) => value(key) === "Yes";
+ switch (type) {
+ case EventType.LUNCH:
+ return {
+ budget: value("budget"),
+ foodPreferencesEnabled: yes("foodPreferencesEnabled"),
+ };
+ case EventType.DINNER:
+ return {
+ budget: value("budget"),
+ plusOneAllowed: yes("plusOneAllowed"),
+ foodPreferencesEnabled: yes("foodPreferencesEnabled"),
+ };
+ case EventType.POTLUCK:
+ return {
+ neededCategories: value("neededCategories")
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean),
+ };
+ case EventType.WEEKEND_RUN:
+ return {
+ distance:
+ value("distance") === "Custom" ? value("customDistance") : value("distance"),
+ pace: value("pace"),
+ routeLink: value("routeLink"),
+ beginnerFriendly: yes("beginnerFriendly"),
+ };
+ case EventType.MARATHON:
+ return {
+ raceName: value("raceName"),
+ raceDate: value("raceDate"),
+ distance:
+ value("distance") === "Custom" ? value("customDistance") : value("distance"),
+ targetPace: value("targetPace"),
+ beginnerFriendly: yes("beginnerFriendly"),
+ };
+ case EventType.HYROX:
+ return {
+ workoutType: value("workoutType"),
+ partnerNeeded: yes("partnerNeeded"),
+ beginnerFriendly: yes("beginnerFriendly"),
+ intensity: value("intensity"),
+ };
+ }
+}
+
+export function validatedInputForTest(
+ input: Record,
+): ValidatedEventInput {
+ return eventInputSchema.parse(input);
+}
diff --git a/src/interactions/fitness.actions.ts b/src/interactions/fitness.actions.ts
new file mode 100644
index 0000000..68d9e5d
--- /dev/null
+++ b/src/interactions/fitness.actions.ts
@@ -0,0 +1,9 @@
+import { EventType } from "@prisma/client";
+
+export function isFitnessEvent(type: EventType): boolean {
+ return (
+ type === EventType.WEEKEND_RUN ||
+ type === EventType.MARATHON ||
+ type === EventType.HYROX
+ );
+}
diff --git a/src/interactions/helpers.ts b/src/interactions/helpers.ts
new file mode 100644
index 0000000..784dbe6
--- /dev/null
+++ b/src/interactions/helpers.ts
@@ -0,0 +1,52 @@
+import type { SlackViewValues } from "../types/slack.types.js";
+
+export function parseActionValue(action: unknown): { eventId: string; value?: string } {
+ if (
+ !action ||
+ typeof action !== "object" ||
+ !("value" in action) ||
+ typeof action.value !== "string"
+ ) {
+ throw new Error("Missing action value");
+ }
+ return JSON.parse(action.value) as { eventId: string; value?: string };
+}
+
+export function modalValue(
+ values: SlackViewValues,
+ blockId: string,
+ actionId = "value",
+): string {
+ const state = values[blockId]?.[actionId];
+ if (!state) return "";
+ if ("value" in state && typeof state.value === "string") return state.value;
+ if (
+ "selected_option" in state &&
+ state.selected_option &&
+ "value" in state.selected_option
+ ) {
+ return state.selected_option.value;
+ }
+ if ("selected_date" in state && typeof state.selected_date === "string") {
+ return state.selected_date;
+ }
+ if ("selected_time" in state && typeof state.selected_time === "string") {
+ return state.selected_time;
+ }
+ return "";
+}
+
+export function channelIdFromBody(body: unknown): string {
+ if (
+ body &&
+ typeof body === "object" &&
+ "channel" in body &&
+ body.channel &&
+ typeof body.channel === "object" &&
+ "id" in body.channel &&
+ typeof body.channel.id === "string"
+ ) {
+ return body.channel.id;
+ }
+ throw new Error("Missing Slack channel");
+}
diff --git a/src/interactions/organizer.actions.ts b/src/interactions/organizer.actions.ts
new file mode 100644
index 0000000..cdda94a
--- /dev/null
+++ b/src/interactions/organizer.actions.ts
@@ -0,0 +1,133 @@
+import { ReminderType } from "@prisma/client";
+import type { App } from "@slack/bolt";
+import { renderSummary } from "../blocks/summary.blocks.js";
+import { buildEditEventModal } from "../modals/edit-event.modal.js";
+import { EventService } from "../services/event.service.js";
+import { PermissionService } from "../services/permission.service.js";
+import { ReminderService } from "../services/reminder.service.js";
+import { SlackMessageService } from "../services/slack-message.service.js";
+import { slackTime } from "../utils/date.js";
+import { toUserMessage } from "../utils/errors.js";
+import { eventInputSchema, formatZodErrors } from "../utils/validation.js";
+import { extractActivityValues } from "../modals/shared.modal.js";
+import { parseActionValue } from "./helpers.js";
+
+export function registerOrganizerActions(
+ app: App,
+ events: EventService,
+ reminders: ReminderService,
+ messages: SlackMessageService,
+): void {
+ const permissions = new PermissionService();
+
+ app.action("view_summary", async ({ ack, action, body, client, respond }) => {
+ await ack();
+ try {
+ const event = await events.getById(parseActionValue(action).eventId);
+ await client.chat.postEphemeral({
+ channel: event.slackChannelId,
+ user: body.user.id,
+ text: `Summary for ${event.title}`,
+ blocks: renderSummary(event),
+ });
+ } catch (error) {
+ await respond({ response_type: "ephemeral", text: toUserMessage(error) });
+ }
+ });
+
+ app.action("edit_event", async ({ ack, action, body, client, respond }) => {
+ await ack();
+ try {
+ const event = await events.getById(parseActionValue(action).eventId);
+ permissions.assertOrganizer(event, body.user.id);
+ if (!("trigger_id" in body) || typeof body.trigger_id !== "string") {
+ throw new Error("Missing trigger ID");
+ }
+ await client.views.open({
+ trigger_id: body.trigger_id,
+ view: buildEditEventModal(event),
+ });
+ } catch (error) {
+ await respond({ response_type: "ephemeral", text: toUserMessage(error) });
+ }
+ });
+
+ app.view("edit_event_submission", async ({ ack, body, view, client }) => {
+ const raw = extractActivityValues(view.state.values, []);
+ const parsed = eventInputSchema.safeParse(raw);
+ if (!parsed.success) {
+ await ack({
+ response_action: "errors",
+ errors: formatZodErrors(parsed.error),
+ });
+ return;
+ }
+ await ack();
+ const { eventId } = JSON.parse(view.private_metadata) as { eventId: string };
+ const event = await events.update(eventId, body.user.id, {
+ title: parsed.data.title,
+ location: parsed.data.location,
+ description: parsed.data.notes || null,
+ eventStartsAt: parsed.data.eventStartsAt,
+ rsvpDeadlineAt: parsed.data.rsvpDeadlineAt,
+ });
+ await messages.updateEvent(client, event);
+ });
+
+ app.action("close_rsvp", async ({ ack, action, body, client, respond }) => {
+ await ack();
+ try {
+ const event = await events.closeRsvp(
+ parseActionValue(action).eventId,
+ body.user.id,
+ );
+ await messages.updateEvent(client, event);
+ await respond({ response_type: "ephemeral", text: "RSVPs are now closed." });
+ } catch (error) {
+ await respond({ response_type: "ephemeral", text: toUserMessage(error) });
+ }
+ });
+
+ app.action("cancel_event", async ({ ack, action, body, client, respond }) => {
+ await ack();
+ try {
+ const event = await events.cancel(parseActionValue(action).eventId, body.user.id);
+ await messages.updateEvent(client, event);
+ await respond({
+ response_type: "ephemeral",
+ text: "The event has been cancelled.",
+ });
+ } catch (error) {
+ await respond({ response_type: "ephemeral", text: toUserMessage(error) });
+ }
+ });
+
+ app.action("send_reminder", async ({ ack, action, body, client, respond }) => {
+ await ack();
+ try {
+ const event = await events.getById(parseActionValue(action).eventId);
+ permissions.assertOrganizer(event, body.user.id);
+ const reminder = await reminders.createManual(event.id);
+ try {
+ await client.chat.postMessage({
+ channel: event.slackChannelId,
+ ...(event.slackMessageTs ? { thread_ts: event.slackMessageTs } : {}),
+ text: `🔔 Reminder from <@${body.user.id}>: Please update your RSVP for *${event.title}*. The event starts ${slackTime(event.eventStartsAt)}.`,
+ });
+ await reminders.markSent(reminder.id);
+ } catch (error) {
+ await reminders.markFailed(reminder.id, error);
+ throw error;
+ }
+ await respond({ response_type: "ephemeral", text: "Reminder sent." });
+ } catch (error) {
+ await respond({ response_type: "ephemeral", text: toUserMessage(error) });
+ }
+ });
+}
+
+export function reminderLabel(type: ReminderType): string {
+ if (type === ReminderType.RSVP_DEADLINE) return "RSVP deadline";
+ if (type === ReminderType.EVENT_START) return "Event start";
+ return "Manual";
+}
diff --git a/src/interactions/potluck.actions.ts b/src/interactions/potluck.actions.ts
new file mode 100644
index 0000000..446732f
--- /dev/null
+++ b/src/interactions/potluck.actions.ts
@@ -0,0 +1,68 @@
+import type { App } from "@slack/bolt";
+import { buildPotluckItemModal } from "../modals/potluck-item.modal.js";
+import { EventService } from "../services/event.service.js";
+import { PotluckService } from "../services/potluck.service.js";
+import { SlackMessageService } from "../services/slack-message.service.js";
+import { toUserMessage } from "../utils/errors.js";
+import { modalValue, parseActionValue } from "./helpers.js";
+
+export function registerPotluckActions(
+ app: App,
+ events: EventService,
+ potluck: PotluckService,
+ messages: SlackMessageService,
+): void {
+ app.action("open_potluck_item", async ({ ack, action, body, client, respond }) => {
+ await ack();
+ try {
+ const { eventId } = parseActionValue(action);
+ const event = await events.getById(eventId);
+ if (!("trigger_id" in body) || typeof body.trigger_id !== "string") {
+ throw new Error("Missing trigger ID");
+ }
+ await client.views.open({
+ trigger_id: body.trigger_id,
+ view: buildPotluckItemModal(event),
+ });
+ } catch (error) {
+ await respond({ response_type: "ephemeral", text: toUserMessage(error) });
+ }
+ });
+
+ app.view("save_potluck_item", async ({ ack, body, view, client }) => {
+ const values = view.state.values;
+ const itemName = modalValue(values, "itemName").trim();
+ const servesRaw = modalValue(values, "servesCount").trim();
+ const servesCount = servesRaw ? Number.parseInt(servesRaw, 10) : undefined;
+ const errors: Record = {};
+ if (!itemName) errors.itemName = "Enter a dish or item name.";
+ if (servesRaw && (!servesCount || servesCount < 1 || servesCount > 10_000)) {
+ errors.servesCount = "Enter a positive number.";
+ }
+ if (Object.keys(errors).length) {
+ await ack({ response_action: "errors", errors });
+ return;
+ }
+ await ack();
+ const { eventId } = JSON.parse(view.private_metadata) as { eventId: string };
+ await potluck.addItem(eventId, body.user.id, {
+ category: modalValue(values, "category"),
+ itemName,
+ ...(servesCount ? { servesCount } : {}),
+ ...(modalValue(values, "dietaryType")
+ ? { dietaryType: modalValue(values, "dietaryType") }
+ : {}),
+ ...(modalValue(values, "itemNotes")
+ ? { notes: modalValue(values, "itemNotes") }
+ : {}),
+ });
+ const event = await events.getById(eventId);
+ await messages.updateEvent(client, event);
+ await messages.ephemeral(
+ client,
+ event.slackChannelId,
+ body.user.id,
+ "Added your potluck item.",
+ );
+ });
+}
diff --git a/src/interactions/preference.actions.ts b/src/interactions/preference.actions.ts
new file mode 100644
index 0000000..a16a8c4
--- /dev/null
+++ b/src/interactions/preference.actions.ts
@@ -0,0 +1,63 @@
+import type { App } from "@slack/bolt";
+import { buildPreferenceModal } from "../modals/preference.modal.js";
+import { EventService } from "../services/event.service.js";
+import { PreferenceService } from "../services/preference.service.js";
+import { SlackMessageService } from "../services/slack-message.service.js";
+import { toUserMessage } from "../utils/errors.js";
+import { modalValue, parseActionValue } from "./helpers.js";
+
+export function registerPreferenceActions(
+ app: App,
+ events: EventService,
+ preferences: PreferenceService,
+ messages: SlackMessageService,
+): void {
+ app.action("open_preference", async ({ ack, action, body, client, respond }) => {
+ await ack();
+ try {
+ const { eventId } = parseActionValue(action);
+ const event = await events.getById(eventId);
+ if (!("trigger_id" in body) || typeof body.trigger_id !== "string") {
+ throw new Error("Missing trigger ID");
+ }
+ await client.views.open({
+ trigger_id: body.trigger_id,
+ view: buildPreferenceModal(event),
+ });
+ } catch (error) {
+ await respond({ response_type: "ephemeral", text: toUserMessage(error) });
+ }
+ });
+
+ app.view("save_preference", async ({ ack, body, view, client }) => {
+ const values = view.state.values;
+ const preference = modalValue(values, "preference");
+ const metadata = JSON.parse(view.private_metadata) as {
+ eventId: string;
+ primaryRequired?: boolean;
+ };
+ if (metadata.primaryRequired !== false && !preference) {
+ await ack({
+ response_action: "errors",
+ errors: { preference: "Choose a preference." },
+ });
+ return;
+ }
+ await ack();
+ const { eventId } = metadata;
+ if (preference) {
+ await preferences.upsert(eventId, body.user.id, "primary", preference);
+ }
+ const plusOne = modalValue(values, "plusOne");
+ if (plusOne) {
+ await preferences.upsert(eventId, body.user.id, "plusOne", plusOne);
+ }
+ const targetPace = modalValue(values, "targetPaceNote");
+ if (targetPace) {
+ await preferences.upsert(eventId, body.user.id, "targetPace", targetPace);
+ }
+ const event = await events.getById(eventId);
+ await messages.updateEvent(client, event);
+ await messages.ephemeral(client, event.slackChannelId, body.user.id, "Saved.");
+ });
+}
diff --git a/src/interactions/rsvp.actions.ts b/src/interactions/rsvp.actions.ts
new file mode 100644
index 0000000..1951cb8
--- /dev/null
+++ b/src/interactions/rsvp.actions.ts
@@ -0,0 +1,47 @@
+import { RsvpStatus } from "@prisma/client";
+import type { App } from "@slack/bolt";
+import { EventService } from "../services/event.service.js";
+import { RsvpService } from "../services/rsvp.service.js";
+import { SlackMessageService } from "../services/slack-message.service.js";
+import { toUserMessage } from "../utils/errors.js";
+import { parseActionValue } from "./helpers.js";
+
+const actions = [
+ {
+ actionId: "rsvp_going",
+ status: RsvpStatus.GOING,
+ confirmation: "You’re marked as going.",
+ },
+ {
+ actionId: "rsvp_maybe",
+ status: RsvpStatus.MAYBE,
+ confirmation: "You’re marked as maybe.",
+ },
+ {
+ actionId: "rsvp_cannot",
+ status: RsvpStatus.CANNOT_MAKE_IT,
+ confirmation: "You’re marked as unable to make it.",
+ },
+] as const;
+
+export function registerRsvpActions(
+ app: App,
+ rsvpService: RsvpService,
+ eventService: EventService,
+ messages: SlackMessageService,
+): void {
+ for (const definition of actions) {
+ app.action(definition.actionId, async ({ ack, action, body, client, respond }) => {
+ await ack();
+ try {
+ const { eventId } = parseActionValue(action);
+ await rsvpService.upsert(eventId, body.user.id, definition.status);
+ const event = await eventService.getById(eventId);
+ await messages.updateEvent(client, event);
+ await respond({ response_type: "ephemeral", text: definition.confirmation });
+ } catch (error) {
+ await respond({ response_type: "ephemeral", text: toUserMessage(error) });
+ }
+ });
+ }
+}
diff --git a/src/jobs/reminder.job.ts b/src/jobs/reminder.job.ts
new file mode 100644
index 0000000..7878bb6
--- /dev/null
+++ b/src/jobs/reminder.job.ts
@@ -0,0 +1,63 @@
+import { ReminderType } from "@prisma/client";
+import { WebClient } from "@slack/web-api";
+import cron, { type ScheduledTask } from "node-cron";
+import type { Env } from "../config/env.js";
+import type { Logger } from "../config/logger.js";
+import { ReminderService } from "../services/reminder.service.js";
+import { WorkspaceService } from "../services/workspace.service.js";
+import { slackTime } from "../utils/date.js";
+
+export function startReminderJob(
+ reminders: ReminderService,
+ workspaces: WorkspaceService,
+ env: Env,
+ logger: Logger,
+): ScheduledTask {
+ return cron.schedule(
+ "* * * * *",
+ async () => {
+ const due = await reminders.due();
+ for (const reminder of due) {
+ if (!(await reminders.claim(reminder.id))) continue;
+ try {
+ const token =
+ env.SLACK_BOT_TOKEN ??
+ (await workspaces.getBotTokenByWorkspaceId(reminder.event.workspaceId));
+ if (!token) throw new Error("No Slack bot token for reminder workspace.");
+ const client = new WebClient(token);
+ await client.chat.postMessage({
+ channel: reminder.event.slackChannelId,
+ ...(reminder.event.slackMessageTs
+ ? { thread_ts: reminder.event.slackMessageTs }
+ : {}),
+ text: reminderText(reminder.type, reminder.event),
+ });
+ await reminders.markSent(reminder.id);
+ } catch (error) {
+ logger.error("Reminder delivery failed", {
+ reminderId: reminder.id,
+ eventId: reminder.eventId,
+ error: error instanceof Error ? error.message : String(error),
+ });
+ await reminders.markFailed(reminder.id, error);
+ }
+ }
+ },
+ { noOverlap: true },
+ );
+}
+
+function reminderText(
+ type: ReminderType,
+ event: { title: string; location: string | null; eventStartsAt: Date },
+): string {
+ if (type === ReminderType.RSVP_DEADLINE) {
+ return `⏰ Reminder: RSVP for *${event.title}* soon.`;
+ }
+ if (type === ReminderType.EVENT_START) {
+ return `📍 *${event.title}* starts ${slackTime(event.eventStartsAt)}${
+ event.location ? ` at ${event.location}` : ""
+ }. See you there!`;
+ }
+ return `🔔 Reminder: Please update your RSVP for *${event.title}*.`;
+}
diff --git a/src/modals/dinner.modal.ts b/src/modals/dinner.modal.ts
new file mode 100644
index 0000000..dafa5da
--- /dev/null
+++ b/src/modals/dinner.modal.ts
@@ -0,0 +1,32 @@
+import { EventType } from "@prisma/client";
+import type { CommandMetadata } from "../types/slack.types.js";
+import {
+ buildActivityModal,
+ callbackIdFor,
+ type ActivityModalConfig,
+} from "./shared.modal.js";
+
+export const dinnerModalConfig: ActivityModalConfig = {
+ callbackId: callbackIdFor(EventType.DINNER),
+ title: "Create team dinner",
+ defaultTitle: "Team Dinner",
+ locationLabel: "Venue",
+ fields: [
+ { blockId: "budget", label: "Budget per person", kind: "text", optional: true },
+ {
+ blockId: "plusOneAllowed",
+ label: "Plus-one allowed?",
+ kind: "boolean",
+ initialValue: "No",
+ },
+ {
+ blockId: "foodPreferencesEnabled",
+ label: "Food preference options enabled?",
+ kind: "boolean",
+ initialValue: "Yes",
+ },
+ ],
+};
+
+export const buildDinnerModal = (metadata: CommandMetadata) =>
+ buildActivityModal(dinnerModalConfig, metadata);
diff --git a/src/modals/edit-event.modal.ts b/src/modals/edit-event.modal.ts
new file mode 100644
index 0000000..b700105
--- /dev/null
+++ b/src/modals/edit-event.modal.ts
@@ -0,0 +1,83 @@
+import type { ModalView } from "@slack/types";
+import type { EventWithRelations } from "../types/event.types.js";
+
+export function buildEditEventModal(event: EventWithRelations): ModalView {
+ return {
+ type: "modal",
+ callback_id: "edit_event_submission",
+ private_metadata: JSON.stringify({ eventId: event.id }),
+ title: { type: "plain_text", text: "Edit event" },
+ submit: { type: "plain_text", text: "Save changes" },
+ close: { type: "plain_text", text: "Cancel" },
+ blocks: [
+ textInput("title", "Activity title", event.title),
+ {
+ type: "input",
+ block_id: "date",
+ label: { type: "plain_text", text: "Date" },
+ element: {
+ type: "datepicker",
+ action_id: "value",
+ initial_date: event.eventStartsAt.toISOString().slice(0, 10),
+ },
+ },
+ {
+ type: "input",
+ block_id: "time",
+ label: { type: "plain_text", text: "Time (UTC)" },
+ element: {
+ type: "timepicker",
+ action_id: "value",
+ initial_time: event.eventStartsAt.toISOString().slice(11, 16),
+ },
+ },
+ textInput("location", "Location / meeting point", event.location ?? ""),
+ {
+ type: "input",
+ block_id: "rsvpDeadlineDate",
+ label: { type: "plain_text", text: "RSVP deadline date" },
+ element: {
+ type: "datepicker",
+ action_id: "value",
+ initial_date: (event.rsvpDeadlineAt ?? event.eventStartsAt)
+ .toISOString()
+ .slice(0, 10),
+ },
+ },
+ {
+ type: "input",
+ block_id: "rsvpDeadlineTime",
+ label: { type: "plain_text", text: "RSVP deadline time (UTC)" },
+ element: {
+ type: "timepicker",
+ action_id: "value",
+ initial_time: (event.rsvpDeadlineAt ?? event.eventStartsAt)
+ .toISOString()
+ .slice(11, 16),
+ },
+ },
+ textInput("notes", "Notes", event.description ?? "", true, true),
+ ],
+ };
+}
+
+function textInput(
+ blockId: string,
+ label: string,
+ initialValue: string,
+ optional = false,
+ multiline = false,
+) {
+ return {
+ type: "input" as const,
+ block_id: blockId,
+ optional,
+ label: { type: "plain_text" as const, text: label },
+ element: {
+ type: "plain_text_input" as const,
+ action_id: "value",
+ initial_value: initialValue,
+ multiline,
+ },
+ };
+}
diff --git a/src/modals/hyrox.modal.ts b/src/modals/hyrox.modal.ts
new file mode 100644
index 0000000..75386a2
--- /dev/null
+++ b/src/modals/hyrox.modal.ts
@@ -0,0 +1,45 @@
+import { EventType } from "@prisma/client";
+import type { CommandMetadata } from "../types/slack.types.js";
+import {
+ buildActivityModal,
+ callbackIdFor,
+ type ActivityModalConfig,
+} from "./shared.modal.js";
+
+export const hyroxModalConfig: ActivityModalConfig = {
+ callbackId: callbackIdFor(EventType.HYROX),
+ title: "Create HYROX session",
+ defaultTitle: "HYROX Session",
+ locationLabel: "Gym / location",
+ fields: [
+ {
+ blockId: "workoutType",
+ label: "Workout type",
+ kind: "select",
+ options: ["Simulation", "Running", "Strength", "Full prep", "Custom"],
+ initialValue: "Full prep",
+ },
+ {
+ blockId: "partnerNeeded",
+ label: "Partner needed?",
+ kind: "boolean",
+ initialValue: "No",
+ },
+ {
+ blockId: "beginnerFriendly",
+ label: "Beginner friendly?",
+ kind: "boolean",
+ initialValue: "Yes",
+ },
+ {
+ blockId: "intensity",
+ label: "Intensity",
+ kind: "select",
+ options: ["Casual", "Competitive", "Mixed"],
+ initialValue: "Mixed",
+ },
+ ],
+};
+
+export const buildHyroxModal = (metadata: CommandMetadata) =>
+ buildActivityModal(hyroxModalConfig, metadata);
diff --git a/src/modals/lunch.modal.ts b/src/modals/lunch.modal.ts
new file mode 100644
index 0000000..09a0851
--- /dev/null
+++ b/src/modals/lunch.modal.ts
@@ -0,0 +1,26 @@
+import { EventType } from "@prisma/client";
+import type { CommandMetadata } from "../types/slack.types.js";
+import {
+ buildActivityModal,
+ callbackIdFor,
+ type ActivityModalConfig,
+} from "./shared.modal.js";
+
+export const lunchModalConfig: ActivityModalConfig = {
+ callbackId: callbackIdFor(EventType.LUNCH),
+ title: "Create team lunch",
+ defaultTitle: "Team Lunch",
+ locationLabel: "Restaurant / location / order link",
+ fields: [
+ { blockId: "budget", label: "Budget per person", kind: "text", optional: true },
+ {
+ blockId: "foodPreferencesEnabled",
+ label: "Food preference options enabled?",
+ kind: "boolean",
+ initialValue: "Yes",
+ },
+ ],
+};
+
+export const buildLunchModal = (metadata: CommandMetadata) =>
+ buildActivityModal(lunchModalConfig, metadata);
diff --git a/src/modals/marathon.modal.ts b/src/modals/marathon.modal.ts
new file mode 100644
index 0000000..05ef73d
--- /dev/null
+++ b/src/modals/marathon.modal.ts
@@ -0,0 +1,46 @@
+import { EventType } from "@prisma/client";
+import type { CommandMetadata } from "../types/slack.types.js";
+import {
+ buildActivityModal,
+ callbackIdFor,
+ type ActivityModalConfig,
+} from "./shared.modal.js";
+
+export const marathonModalConfig: ActivityModalConfig = {
+ callbackId: callbackIdFor(EventType.MARATHON),
+ title: "Create marathon plan",
+ defaultTitle: "Marathon Training",
+ locationLabel: "Meeting point",
+ fields: [
+ { blockId: "raceName", label: "Race name", kind: "text", optional: true },
+ {
+ blockId: "raceDate",
+ label: "Race date (YYYY-MM-DD)",
+ kind: "text",
+ optional: true,
+ },
+ {
+ blockId: "distance",
+ label: "Distance",
+ kind: "select",
+ options: ["5K", "10K", "21K", "42K", "Custom"],
+ initialValue: "10K",
+ },
+ {
+ blockId: "customDistance",
+ label: "Custom distance",
+ kind: "text",
+ optional: true,
+ },
+ { blockId: "targetPace", label: "Target pace", kind: "text", optional: true },
+ {
+ blockId: "beginnerFriendly",
+ label: "Beginner group available?",
+ kind: "boolean",
+ initialValue: "Yes",
+ },
+ ],
+};
+
+export const buildMarathonModal = (metadata: CommandMetadata) =>
+ buildActivityModal(marathonModalConfig, metadata);
diff --git a/src/modals/potluck-item.modal.ts b/src/modals/potluck-item.modal.ts
new file mode 100644
index 0000000..ce455c1
--- /dev/null
+++ b/src/modals/potluck-item.modal.ts
@@ -0,0 +1,59 @@
+import type { ModalView } from "@slack/types";
+import type { EventWithRelations } from "../types/event.types.js";
+
+export function buildPotluckItemModal(event: EventWithRelations): ModalView {
+ const metadata = event.metadata as { neededCategories?: string[] };
+ const categories = metadata.neededCategories?.length
+ ? metadata.neededCategories
+ : ["Main dish", "Snacks", "Dessert", "Drinks", "Other"];
+ return {
+ type: "modal",
+ callback_id: "save_potluck_item",
+ private_metadata: JSON.stringify({ eventId: event.id }),
+ title: { type: "plain_text", text: "Bring something" },
+ submit: { type: "plain_text", text: "Add item" },
+ close: { type: "plain_text", text: "Cancel" },
+ blocks: [
+ {
+ type: "input",
+ block_id: "category",
+ label: { type: "plain_text", text: "Category" },
+ element: {
+ type: "static_select",
+ action_id: "value",
+ options: categories.slice(0, 100).map((category) => ({
+ text: { type: "plain_text", text: category.slice(0, 75) },
+ value: category.slice(0, 75),
+ })),
+ },
+ },
+ textInput("itemName", "Dish / item name"),
+ textInput("servesCount", "Serves how many", true),
+ {
+ type: "input",
+ block_id: "dietaryType",
+ optional: true,
+ label: { type: "plain_text", text: "Dietary type" },
+ element: {
+ type: "static_select",
+ action_id: "value",
+ options: ["Veg", "Non-veg", "Vegan", "Other"].map((value) => ({
+ text: { type: "plain_text", text: value },
+ value,
+ })),
+ },
+ },
+ textInput("itemNotes", "Notes", true),
+ ],
+ };
+}
+
+function textInput(blockId: string, label: string, optional = false) {
+ return {
+ type: "input" as const,
+ block_id: blockId,
+ optional,
+ label: { type: "plain_text" as const, text: label },
+ element: { type: "plain_text_input" as const, action_id: "value" },
+ };
+}
diff --git a/src/modals/potluck.modal.ts b/src/modals/potluck.modal.ts
new file mode 100644
index 0000000..3bc4b15
--- /dev/null
+++ b/src/modals/potluck.modal.ts
@@ -0,0 +1,26 @@
+import { EventType } from "@prisma/client";
+import type { CommandMetadata } from "../types/slack.types.js";
+import {
+ buildActivityModal,
+ callbackIdFor,
+ type ActivityModalConfig,
+} from "./shared.modal.js";
+
+export const potluckModalConfig: ActivityModalConfig = {
+ callbackId: callbackIdFor(EventType.POTLUCK),
+ title: "Create team potluck",
+ defaultTitle: "Team Potluck",
+ locationLabel: "Venue",
+ fields: [
+ {
+ blockId: "neededCategories",
+ label: "Needed categories",
+ kind: "text",
+ initialValue: "Main dish, Snacks, Dessert, Drinks",
+ placeholder: "Comma-separated categories",
+ },
+ ],
+};
+
+export const buildPotluckModal = (metadata: CommandMetadata) =>
+ buildActivityModal(potluckModalConfig, metadata);
diff --git a/src/modals/preference.modal.ts b/src/modals/preference.modal.ts
new file mode 100644
index 0000000..894505a
--- /dev/null
+++ b/src/modals/preference.modal.ts
@@ -0,0 +1,93 @@
+import { EventType } from "@prisma/client";
+import type { ModalView } from "@slack/types";
+import type { EventWithRelations } from "../types/event.types.js";
+
+const options: Record = {
+ LUNCH: ["Veg", "Non-veg", "Vegan", "No preference"],
+ DINNER: ["Veg", "Non-veg", "Vegan", "No preference"],
+ POTLUCK: [],
+ WEEKEND_RUN: ["Easy pace", "Moderate pace", "Fast pace", "Beginner group"],
+ MARATHON: ["Joining training", "Running race", "Beginner group"],
+ HYROX: ["Need partner", "Beginner", "Competitive", "Casual"],
+};
+
+export function buildPreferenceModal(event: EventWithRelations): ModalView {
+ const metadata = event.metadata as {
+ plusOneAllowed?: boolean;
+ foodPreferencesEnabled?: boolean;
+ };
+ const primaryRequired = !(
+ event.type === EventType.DINNER && metadata.foodPreferencesEnabled === false
+ );
+ return {
+ type: "modal",
+ callback_id: "save_preference",
+ private_metadata: JSON.stringify({ eventId: event.id, primaryRequired }),
+ title: { type: "plain_text", text: "Set preference" },
+ submit: { type: "plain_text", text: "Save" },
+ close: { type: "plain_text", text: "Cancel" },
+ blocks: [
+ ...(primaryRequired
+ ? [
+ {
+ type: "input" as const,
+ block_id: "preference",
+ label: { type: "plain_text" as const, text: "Preference" },
+ element: {
+ type: "static_select" as const,
+ action_id: "value",
+ placeholder: {
+ type: "plain_text" as const,
+ text: "Choose a preference",
+ },
+ options: options[event.type].map((value) => ({
+ text: { type: "plain_text" as const, text: value },
+ value,
+ })),
+ },
+ },
+ ]
+ : []),
+ ...(event.type === EventType.DINNER && metadata.plusOneAllowed
+ ? [
+ {
+ type: "input" as const,
+ block_id: "plusOne",
+ label: { type: "plain_text" as const, text: "Bringing a plus-one?" },
+ element: {
+ type: "static_select" as const,
+ action_id: "value",
+ initial_option: selectOption("No"),
+ options: [selectOption("Yes"), selectOption("No")],
+ },
+ },
+ ]
+ : []),
+ ...(event.type === EventType.MARATHON
+ ? [
+ {
+ type: "input" as const,
+ block_id: "targetPaceNote",
+ optional: true,
+ label: { type: "plain_text" as const, text: "Target pace note" },
+ element: {
+ type: "plain_text_input" as const,
+ action_id: "value",
+ placeholder: {
+ type: "plain_text" as const,
+ text: "Optional, e.g. 5:45/km",
+ },
+ },
+ },
+ ]
+ : []),
+ ],
+ };
+}
+
+function selectOption(value: string) {
+ return {
+ text: { type: "plain_text" as const, text: value },
+ value,
+ };
+}
diff --git a/src/modals/shared.modal.ts b/src/modals/shared.modal.ts
new file mode 100644
index 0000000..0d0f903
--- /dev/null
+++ b/src/modals/shared.modal.ts
@@ -0,0 +1,221 @@
+import type { EventType } from "@prisma/client";
+import type { InputBlockElement, ModalView, KnownBlock } from "@slack/types";
+import type { CommandMetadata, SlackViewValues } from "../types/slack.types.js";
+
+export interface ModalField {
+ blockId: string;
+ actionId?: string;
+ label: string;
+ kind: "text" | "select" | "boolean";
+ optional?: boolean;
+ multiline?: boolean;
+ placeholder?: string;
+ initialValue?: string;
+ options?: string[];
+}
+
+export interface ActivityModalConfig {
+ callbackId: string;
+ title: string;
+ defaultTitle: string;
+ locationLabel: string;
+ fields: ModalField[];
+}
+
+export function buildActivityModal(
+ config: ActivityModalConfig,
+ metadata: CommandMetadata,
+): ModalView {
+ const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000)
+ .toISOString()
+ .slice(0, 10);
+ const blocks: KnownBlock[] = [
+ input("title", "Activity title", {
+ type: "plain_text_input",
+ action_id: "value",
+ initial_value: config.defaultTitle,
+ max_length: 150,
+ }),
+ input("date", "Date", {
+ type: "datepicker",
+ action_id: "value",
+ initial_date: tomorrow,
+ placeholder: { type: "plain_text", text: "Select a date" },
+ }),
+ input("time", "Time (UTC)", {
+ type: "timepicker",
+ action_id: "value",
+ initial_time: "12:30",
+ placeholder: { type: "plain_text", text: "Select a time" },
+ }),
+ input("location", config.locationLabel, {
+ type: "plain_text_input",
+ action_id: "value",
+ max_length: 300,
+ placeholder: { type: "plain_text", text: "Where should everyone meet?" },
+ }),
+ ...config.fields.map(renderField),
+ input("rsvpDeadlineDate", "RSVP deadline date", {
+ type: "datepicker",
+ action_id: "value",
+ initial_date: tomorrow,
+ placeholder: { type: "plain_text", text: "Select a date" },
+ }),
+ input("rsvpDeadlineTime", "RSVP deadline time (UTC)", {
+ type: "timepicker",
+ action_id: "value",
+ initial_time: "10:00",
+ placeholder: { type: "plain_text", text: "Select a time" },
+ }),
+ input(
+ "reminderMinutes",
+ "Event reminder",
+ {
+ type: "static_select",
+ action_id: "value",
+ initial_option: option("60 minutes before", "60"),
+ options: [
+ option("No event reminder", "0"),
+ option("30 minutes before", "30"),
+ option("60 minutes before", "60"),
+ option("1 day before", "1440"),
+ ],
+ },
+ true,
+ ),
+ input(
+ "notes",
+ "Notes",
+ {
+ type: "plain_text_input",
+ action_id: "value",
+ multiline: true,
+ max_length: 1500,
+ placeholder: {
+ type: "plain_text",
+ text: "Anything else the team should know?",
+ },
+ },
+ true,
+ ),
+ ];
+
+ return {
+ type: "modal",
+ callback_id: config.callbackId,
+ private_metadata: JSON.stringify(metadata),
+ title: { type: "plain_text", text: config.title.slice(0, 24) },
+ submit: { type: "plain_text", text: "Create event" },
+ close: { type: "plain_text", text: "Cancel" },
+ notify_on_close: false,
+ blocks,
+ };
+}
+
+export function extractActivityValues(
+ values: SlackViewValues,
+ extraFields: ModalField[],
+): Record {
+ const result: Record = {
+ title: readValue(values, "title"),
+ date: readValue(values, "date"),
+ time: readValue(values, "time"),
+ location: readValue(values, "location"),
+ rsvpDeadlineDate: readValue(values, "rsvpDeadlineDate"),
+ rsvpDeadlineTime: readValue(values, "rsvpDeadlineTime"),
+ reminderMinutes: readValue(values, "reminderMinutes"),
+ notes: readValue(values, "notes"),
+ };
+ for (const field of extraFields) {
+ result[field.blockId] = readValue(values, field.blockId, field.actionId ?? "value");
+ }
+ return result;
+}
+
+export function callbackIdFor(type: EventType): string {
+ return `create_${type.toLowerCase()}_event`;
+}
+
+function renderField(field: ModalField): KnownBlock {
+ if (field.kind === "select" || field.kind === "boolean") {
+ const options = field.kind === "boolean" ? ["Yes", "No"] : (field.options ?? []);
+ const initialValue = field.initialValue ?? options[0] ?? "";
+ return input(
+ field.blockId,
+ field.label,
+ {
+ type: "static_select",
+ action_id: field.actionId ?? "value",
+ placeholder: {
+ type: "plain_text",
+ text: field.placeholder ?? "Choose an option",
+ },
+ options: options.map((value) => option(value, value)),
+ ...(initialValue ? { initial_option: option(initialValue, initialValue) } : {}),
+ },
+ field.optional,
+ );
+ }
+
+ return input(
+ field.blockId,
+ field.label,
+ {
+ type: "plain_text_input",
+ action_id: field.actionId ?? "value",
+ ...(field.initialValue ? { initial_value: field.initialValue } : {}),
+ ...(field.multiline ? { multiline: true } : {}),
+ placeholder: {
+ type: "plain_text",
+ text: field.placeholder ?? `Enter ${field.label.toLowerCase()}`,
+ },
+ },
+ field.optional,
+ );
+}
+
+function input(
+ blockId: string,
+ label: string,
+ element: InputBlockElement,
+ optional = false,
+): KnownBlock {
+ return {
+ type: "input",
+ block_id: blockId,
+ optional,
+ label: { type: "plain_text", text: label, emoji: true },
+ element,
+ };
+}
+
+function option(text: string, value: string) {
+ return {
+ text: { type: "plain_text" as const, text, emoji: true },
+ value,
+ };
+}
+
+function readValue(
+ values: SlackViewValues,
+ blockId: string,
+ actionId = "value",
+): string {
+ const state = values[blockId]?.[actionId];
+ if (!state) return "";
+ if ("value" in state && typeof state.value === "string") return state.value;
+ if ("selected_date" in state && typeof state.selected_date === "string") {
+ return state.selected_date;
+ }
+ if ("selected_time" in state && typeof state.selected_time === "string") {
+ return state.selected_time;
+ }
+ if (
+ "selected_option" in state &&
+ state.selected_option &&
+ "value" in state.selected_option
+ ) {
+ return state.selected_option.value;
+ }
+ return "";
+}
diff --git a/src/modals/weekendrun.modal.ts b/src/modals/weekendrun.modal.ts
new file mode 100644
index 0000000..de14b3d
--- /dev/null
+++ b/src/modals/weekendrun.modal.ts
@@ -0,0 +1,46 @@
+import { EventType } from "@prisma/client";
+import type { CommandMetadata } from "../types/slack.types.js";
+import {
+ buildActivityModal,
+ callbackIdFor,
+ type ActivityModalConfig,
+} from "./shared.modal.js";
+
+export const weekendRunModalConfig: ActivityModalConfig = {
+ callbackId: callbackIdFor(EventType.WEEKEND_RUN),
+ title: "Create weekend run",
+ defaultTitle: "Weekend Run",
+ locationLabel: "Meeting point",
+ fields: [
+ {
+ blockId: "distance",
+ label: "Distance",
+ kind: "select",
+ options: ["3K", "5K", "10K", "Custom"],
+ initialValue: "5K",
+ },
+ {
+ blockId: "customDistance",
+ label: "Custom distance",
+ kind: "text",
+ optional: true,
+ },
+ {
+ blockId: "pace",
+ label: "Pace",
+ kind: "select",
+ options: ["Easy", "Moderate", "Fast", "Mixed groups"],
+ initialValue: "Mixed groups",
+ },
+ { blockId: "routeLink", label: "Route link", kind: "text", optional: true },
+ {
+ blockId: "beginnerFriendly",
+ label: "Beginner friendly?",
+ kind: "boolean",
+ initialValue: "Yes",
+ },
+ ],
+};
+
+export const buildWeekendRunModal = (metadata: CommandMetadata) =>
+ buildActivityModal(weekendRunModalConfig, metadata);
diff --git a/src/services/event.service.ts b/src/services/event.service.ts
new file mode 100644
index 0000000..cff081d
--- /dev/null
+++ b/src/services/event.service.ts
@@ -0,0 +1,222 @@
+import {
+ EventStatus,
+ ReminderStatus,
+ type Event,
+ type PrismaClient,
+} from "@prisma/client";
+import type {
+ CreateEventInput,
+ EventWithRelations,
+ UpdateEventInput,
+} from "../types/event.types.js";
+import { metadataToJson } from "../types/event.types.js";
+import { NotFoundError } from "../utils/errors.js";
+import { PermissionService } from "./permission.service.js";
+
+export class EventService {
+ constructor(
+ private readonly db: PrismaClient,
+ private readonly permissions = new PermissionService(),
+ ) {}
+
+ async create(input: CreateEventInput): Promise {
+ return this.db.$transaction(async (tx) => {
+ const workspace = await tx.workspace.upsert({
+ where: { slackTeamId: input.slackTeamId },
+ update: {
+ ...(input.slackTeamName ? { name: input.slackTeamName } : {}),
+ uninstalledAt: null,
+ },
+ create: {
+ slackTeamId: input.slackTeamId,
+ ...(input.slackTeamName ? { name: input.slackTeamName } : {}),
+ },
+ });
+
+ const channel = await tx.channel.upsert({
+ where: {
+ workspaceId_slackChannelId: {
+ workspaceId: workspace.id,
+ slackChannelId: input.slackChannelId,
+ },
+ },
+ update: input.slackChannelName ? { name: input.slackChannelName } : {},
+ create: {
+ workspaceId: workspace.id,
+ slackChannelId: input.slackChannelId,
+ ...(input.slackChannelName ? { name: input.slackChannelName } : {}),
+ },
+ });
+
+ const event = await tx.event.create({
+ data: {
+ workspaceId: workspace.id,
+ channelId: channel.id,
+ organizerSlackUserId: input.organizerSlackUserId,
+ type: input.type,
+ title: input.title,
+ ...(input.description ? { description: input.description } : {}),
+ location: input.location,
+ eventStartsAt: input.eventStartsAt,
+ ...(input.rsvpDeadlineAt ? { rsvpDeadlineAt: input.rsvpDeadlineAt } : {}),
+ slackChannelId: input.slackChannelId,
+ metadata: metadataToJson(input.metadata ?? {}),
+ },
+ });
+
+ const neededCategories =
+ input.type === "POTLUCK" && Array.isArray(input.metadata?.neededCategories)
+ ? input.metadata.neededCategories
+ : [];
+ if (neededCategories.length) {
+ await tx.potluckItem.createMany({
+ data: neededCategories.map((category) => ({
+ eventId: event.id,
+ category,
+ itemName: "Needed",
+ })),
+ });
+ }
+
+ await tx.auditLog.create({
+ data: {
+ workspaceId: workspace.id,
+ eventId: event.id,
+ actorSlackUserId: input.organizerSlackUserId,
+ action: "event.created",
+ metadata: { type: input.type },
+ },
+ });
+
+ return tx.event.findUniqueOrThrow({
+ where: { id: event.id },
+ include: this.relations(),
+ });
+ });
+ }
+
+ async getById(eventId: string): Promise {
+ const event = await this.db.event.findUnique({
+ where: { id: eventId },
+ include: this.relations(),
+ });
+ if (!event) throw new NotFoundError();
+ return event;
+ }
+
+ async update(
+ eventId: string,
+ actorSlackUserId: string,
+ input: UpdateEventInput,
+ ): Promise {
+ const existing = await this.getById(eventId);
+ this.permissions.assertOrganizer(existing, actorSlackUserId);
+
+ return this.db.$transaction(async (tx) => {
+ await tx.event.update({
+ where: { id: eventId },
+ data: {
+ ...input,
+ ...(input.metadata ? { metadata: metadataToJson(input.metadata) } : {}),
+ },
+ });
+ await tx.auditLog.create({
+ data: {
+ workspaceId: existing.workspaceId,
+ eventId,
+ actorSlackUserId,
+ action: "event.updated",
+ },
+ });
+ return tx.event.findUniqueOrThrow({
+ where: { id: eventId },
+ include: this.relations(),
+ });
+ });
+ }
+
+ async setSlackMessage(eventId: string, channel: string, ts: string): Promise {
+ return this.db.event.update({
+ where: { id: eventId },
+ data: { slackChannelId: channel, slackMessageTs: ts },
+ });
+ }
+
+ async closeRsvp(
+ eventId: string,
+ actorSlackUserId: string,
+ ): Promise {
+ const existing = await this.getById(eventId);
+ this.permissions.assertOrganizer(existing, actorSlackUserId);
+
+ return this.transition(existing, actorSlackUserId, EventStatus.RSVP_CLOSED, {
+ closedAt: new Date(),
+ action: "event.rsvp_closed",
+ });
+ }
+
+ async cancel(eventId: string, actorSlackUserId: string): Promise {
+ const existing = await this.getById(eventId);
+ this.permissions.assertOrganizer(existing, actorSlackUserId);
+
+ return this.db.$transaction(async (tx) => {
+ await tx.event.update({
+ where: { id: eventId },
+ data: { status: EventStatus.CANCELLED, cancelledAt: new Date() },
+ });
+ await tx.reminder.updateMany({
+ where: { eventId, status: ReminderStatus.PENDING },
+ data: { status: ReminderStatus.CANCELLED },
+ });
+ await tx.auditLog.create({
+ data: {
+ workspaceId: existing.workspaceId,
+ eventId,
+ actorSlackUserId,
+ action: "event.cancelled",
+ },
+ });
+ return tx.event.findUniqueOrThrow({
+ where: { id: eventId },
+ include: this.relations(),
+ });
+ });
+ }
+
+ private async transition(
+ existing: EventWithRelations,
+ actorSlackUserId: string,
+ status: EventStatus,
+ detail: { closedAt?: Date; action: string },
+ ): Promise {
+ return this.db.$transaction(async (tx) => {
+ await tx.event.update({
+ where: { id: existing.id },
+ data: { status, ...(detail.closedAt ? { closedAt: detail.closedAt } : {}) },
+ });
+ await tx.auditLog.create({
+ data: {
+ workspaceId: existing.workspaceId,
+ eventId: existing.id,
+ actorSlackUserId,
+ action: detail.action,
+ },
+ });
+ return tx.event.findUniqueOrThrow({
+ where: { id: existing.id },
+ include: this.relations(),
+ });
+ });
+ }
+
+ private relations() {
+ return {
+ rsvps: true,
+ preferences: true,
+ potluckItems: {
+ where: { claimedBySlackUserId: { not: null } },
+ orderBy: [{ category: "asc" as const }, { createdAt: "asc" as const }],
+ },
+ };
+ }
+}
diff --git a/src/services/oauth-installation.store.ts b/src/services/oauth-installation.store.ts
new file mode 100644
index 0000000..1567933
--- /dev/null
+++ b/src/services/oauth-installation.store.ts
@@ -0,0 +1,58 @@
+import type { Installation, InstallationQuery, InstallationStore } from "@slack/oauth";
+import { SLACK_SCOPES } from "../config/slack.js";
+import { WorkspaceService } from "./workspace.service.js";
+
+export class PrismaInstallationStore implements InstallationStore {
+ constructor(private readonly workspaces: WorkspaceService) {}
+
+ async storeInstallation(
+ installation: Installation,
+ ): Promise {
+ if (!installation.team?.id || !installation.bot?.token) {
+ throw new Error("TeamLoop V1 supports workspace bot installations only.");
+ }
+ await this.workspaces.saveInstallation({
+ teamId: installation.team.id,
+ ...(installation.team.name ? { teamName: installation.team.name } : {}),
+ botToken: installation.bot.token,
+ botUserId: installation.bot.userId,
+ installedBy: installation.user.id,
+ });
+ }
+
+ async fetchInstallation(
+ query: InstallationQuery,
+ ): Promise> {
+ if (!query.teamId) {
+ throw new Error("Enterprise organization installs are not supported in V1.");
+ }
+ const stored = await this.workspaces.getInstallation(query.teamId);
+ if (!stored) throw new Error(`No TeamLoop installation found for ${query.teamId}.`);
+
+ return {
+ authVersion: "v2",
+ isEnterpriseInstall: false,
+ team: {
+ id: stored.teamId,
+ ...(stored.teamName ? { name: stored.teamName } : {}),
+ },
+ enterprise: undefined,
+ user: {
+ id: stored.installedBy ?? "UNKNOWN",
+ token: undefined,
+ scopes: undefined,
+ },
+ bot: {
+ token: stored.botToken,
+ id: stored.botUserId ?? "UNKNOWN",
+ userId: stored.botUserId ?? "UNKNOWN",
+ scopes: [...SLACK_SCOPES],
+ },
+ tokenType: "bot",
+ };
+ }
+
+ async deleteInstallation(query: InstallationQuery): Promise {
+ if (query.teamId) await this.workspaces.markUninstalled(query.teamId);
+ }
+}
diff --git a/src/services/permission.service.ts b/src/services/permission.service.ts
new file mode 100644
index 0000000..c2e503a
--- /dev/null
+++ b/src/services/permission.service.ts
@@ -0,0 +1,13 @@
+import type { Event } from "@prisma/client";
+import { PermissionError } from "../utils/errors.js";
+
+export class PermissionService {
+ assertOrganizer(
+ event: Pick,
+ slackUserId: string,
+ ): void {
+ if (event.organizerSlackUserId !== slackUserId) {
+ throw new PermissionError();
+ }
+ }
+}
diff --git a/src/services/potluck.service.ts b/src/services/potluck.service.ts
new file mode 100644
index 0000000..62571f0
--- /dev/null
+++ b/src/services/potluck.service.ts
@@ -0,0 +1,47 @@
+import { RsvpStatus, type PotluckItem, type PrismaClient } from "@prisma/client";
+import { NotFoundError } from "../utils/errors.js";
+
+export interface PotluckItemInput {
+ category: string;
+ itemName: string;
+ servesCount?: number;
+ dietaryType?: string;
+ notes?: string;
+}
+
+export class PotluckService {
+ constructor(private readonly db: PrismaClient) {}
+
+ async addItem(
+ eventId: string,
+ slackUserId: string,
+ input: PotluckItemInput,
+ ): Promise {
+ const event = await this.db.event.findUnique({
+ where: { id: eventId },
+ select: { id: true, type: true },
+ });
+ if (!event) throw new NotFoundError();
+ if (event.type !== "POTLUCK") throw new Error("Not a potluck event");
+
+ return this.db.$transaction(async (tx) => {
+ const item = await tx.potluckItem.create({
+ data: {
+ eventId,
+ claimedBySlackUserId: slackUserId,
+ category: input.category,
+ itemName: input.itemName,
+ ...(input.servesCount ? { servesCount: input.servesCount } : {}),
+ ...(input.dietaryType ? { dietaryType: input.dietaryType } : {}),
+ ...(input.notes ? { notes: input.notes } : {}),
+ },
+ });
+ await tx.rSVP.upsert({
+ where: { eventId_slackUserId: { eventId, slackUserId } },
+ update: {},
+ create: { eventId, slackUserId, status: RsvpStatus.GOING },
+ });
+ return item;
+ });
+ }
+}
diff --git a/src/services/preference.service.ts b/src/services/preference.service.ts
new file mode 100644
index 0000000..effbc37
--- /dev/null
+++ b/src/services/preference.service.ts
@@ -0,0 +1,27 @@
+import type { PrismaClient, Preference } from "@prisma/client";
+import { NotFoundError } from "../utils/errors.js";
+
+export class PreferenceService {
+ constructor(private readonly db: PrismaClient) {}
+
+ async upsert(
+ eventId: string,
+ slackUserId: string,
+ key: string,
+ value: string,
+ ): Promise {
+ const event = await this.db.event.findUnique({
+ where: { id: eventId },
+ select: { id: true },
+ });
+ if (!event) throw new NotFoundError();
+
+ return this.db.preference.upsert({
+ where: {
+ eventId_slackUserId_key: { eventId, slackUserId, key },
+ },
+ update: { value },
+ create: { eventId, slackUserId, key, value },
+ });
+ }
+}
diff --git a/src/services/reminder.service.ts b/src/services/reminder.service.ts
new file mode 100644
index 0000000..cbd9734
--- /dev/null
+++ b/src/services/reminder.service.ts
@@ -0,0 +1,102 @@
+import {
+ EventStatus,
+ ReminderStatus,
+ ReminderType,
+ type PrismaClient,
+ type Reminder,
+} from "@prisma/client";
+import { minutesBefore } from "../utils/date.js";
+
+export class ReminderService {
+ constructor(private readonly db: PrismaClient) {}
+
+ async createDefaults(input: {
+ eventId: string;
+ eventStartsAt: Date;
+ rsvpDeadlineAt?: Date;
+ eventReminderMinutes?: number;
+ }): Promise {
+ const reminders: Array<{
+ eventId: string;
+ type: ReminderType;
+ scheduledFor: Date;
+ }> = [];
+
+ if (input.rsvpDeadlineAt) {
+ reminders.push({
+ eventId: input.eventId,
+ type: ReminderType.RSVP_DEADLINE,
+ scheduledFor: minutesBefore(input.rsvpDeadlineAt, 60),
+ });
+ }
+ if (input.eventReminderMinutes && input.eventReminderMinutes > 0) {
+ reminders.push({
+ eventId: input.eventId,
+ type: ReminderType.EVENT_START,
+ scheduledFor: minutesBefore(input.eventStartsAt, input.eventReminderMinutes),
+ });
+ }
+
+ for (const reminder of reminders.filter((item) => item.scheduledFor > new Date())) {
+ await this.db.reminder.upsert({
+ where: {
+ eventId_type_scheduledFor: reminder,
+ },
+ update: {},
+ create: reminder,
+ });
+ }
+ }
+
+ async createManual(eventId: string): Promise {
+ return this.db.reminder.create({
+ data: {
+ eventId,
+ type: ReminderType.MANUAL,
+ scheduledFor: new Date(),
+ },
+ });
+ }
+
+ async due(now = new Date(), limit = 50) {
+ return this.db.reminder.findMany({
+ where: {
+ status: ReminderStatus.PENDING,
+ scheduledFor: { lte: now },
+ event: { status: { not: EventStatus.CANCELLED } },
+ },
+ include: { event: true },
+ orderBy: { scheduledFor: "asc" },
+ take: limit,
+ });
+ }
+
+ async claim(reminderId: string): Promise {
+ const result = await this.db.reminder.updateMany({
+ where: { id: reminderId, status: ReminderStatus.PENDING },
+ data: {
+ status: ReminderStatus.PROCESSING,
+ attemptCount: { increment: 1 },
+ },
+ });
+ return result.count === 1;
+ }
+
+ async markSent(reminderId: string): Promise {
+ await this.db.reminder.update({
+ where: { id: reminderId },
+ data: { status: ReminderStatus.SENT, sentAt: new Date(), lastError: null },
+ });
+ }
+
+ async markFailed(reminderId: string, error: unknown): Promise {
+ const message = error instanceof Error ? error.message : "Unknown reminder error";
+ await this.db.reminder.update({
+ where: { id: reminderId },
+ data: {
+ status: ReminderStatus.FAILED,
+ lastError: message.slice(0, 500),
+ },
+ });
+ }
+}
diff --git a/src/services/rsvp.service.ts b/src/services/rsvp.service.ts
new file mode 100644
index 0000000..beb01ea
--- /dev/null
+++ b/src/services/rsvp.service.ts
@@ -0,0 +1,53 @@
+import { EventStatus, RsvpStatus, type PrismaClient, type RSVP } from "@prisma/client";
+import { EventStateError, NotFoundError } from "../utils/errors.js";
+
+export interface RsvpCounts {
+ going: number;
+ maybe: number;
+ cannotMakeIt: number;
+}
+
+export class RsvpService {
+ constructor(private readonly db: PrismaClient) {}
+
+ async upsert(
+ eventId: string,
+ slackUserId: string,
+ status: RsvpStatus,
+ ): Promise {
+ const event = await this.db.event.findUnique({
+ where: { id: eventId },
+ select: { status: true },
+ });
+ if (!event) throw new NotFoundError();
+ if (event.status === EventStatus.CANCELLED) {
+ throw new EventStateError("This event has been cancelled.", "EVENT_CANCELLED");
+ }
+ if (event.status === EventStatus.RSVP_CLOSED) {
+ throw new EventStateError("RSVPs are closed for this event.", "RSVP_CLOSED");
+ }
+
+ return this.db.rSVP.upsert({
+ where: { eventId_slackUserId: { eventId, slackUserId } },
+ update: { status },
+ create: { eventId, slackUserId, status },
+ });
+ }
+
+ async counts(eventId: string): Promise {
+ const rows = await this.db.rSVP.groupBy({
+ by: ["status"],
+ where: { eventId },
+ _count: { _all: true },
+ });
+ const counts: RsvpCounts = { going: 0, maybe: 0, cannotMakeIt: 0 };
+ for (const row of rows) {
+ if (row.status === RsvpStatus.GOING) counts.going = row._count._all;
+ if (row.status === RsvpStatus.MAYBE) counts.maybe = row._count._all;
+ if (row.status === RsvpStatus.CANNOT_MAKE_IT) {
+ counts.cannotMakeIt = row._count._all;
+ }
+ }
+ return counts;
+ }
+}
diff --git a/src/services/slack-message.service.ts b/src/services/slack-message.service.ts
new file mode 100644
index 0000000..8d805a7
--- /dev/null
+++ b/src/services/slack-message.service.ts
@@ -0,0 +1,34 @@
+import type { WebClient } from "@slack/web-api";
+import { renderEventCard } from "../blocks/event-card.blocks.js";
+import type { EventWithRelations } from "../types/event.types.js";
+
+export class SlackMessageService {
+ async postEvent(client: WebClient, event: EventWithRelations) {
+ return client.chat.postMessage({
+ channel: event.slackChannelId,
+ text: `${event.title} — TeamLoop event`,
+ blocks: renderEventCard(event),
+ unfurl_links: false,
+ unfurl_media: false,
+ });
+ }
+
+ async updateEvent(client: WebClient, event: EventWithRelations): Promise {
+ if (!event.slackMessageTs) return;
+ await client.chat.update({
+ channel: event.slackChannelId,
+ ts: event.slackMessageTs,
+ text: `${event.title} — TeamLoop event`,
+ blocks: renderEventCard(event),
+ });
+ }
+
+ async ephemeral(
+ client: WebClient,
+ channel: string,
+ user: string,
+ text: string,
+ ): Promise {
+ await client.chat.postEphemeral({ channel, user, text });
+ }
+}
diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts
new file mode 100644
index 0000000..4e80bf0
--- /dev/null
+++ b/src/services/workspace.service.ts
@@ -0,0 +1,146 @@
+import { ReminderStatus, type PrismaClient } from "@prisma/client";
+import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
+
+export interface InstallationRecord {
+ teamId: string;
+ teamName?: string;
+ botUserId?: string;
+ installedBy?: string;
+ botToken: string;
+}
+
+export interface StoredInstallation {
+ teamId: string;
+ teamName?: string;
+ botUserId?: string;
+ installedBy?: string;
+ botToken: string;
+}
+
+export class WorkspaceService {
+ constructor(
+ private readonly db: PrismaClient,
+ private readonly encryptionKey?: string,
+ ) {}
+
+ async saveInstallation(input: InstallationRecord): Promise {
+ await this.db.workspace.upsert({
+ where: { slackTeamId: input.teamId },
+ update: {
+ ...(input.teamName ? { name: input.teamName } : {}),
+ ...(input.botUserId ? { botUserId: input.botUserId } : {}),
+ ...(input.installedBy ? { installedBySlackUserId: input.installedBy } : {}),
+ accessToken: this.encrypt(input.botToken),
+ uninstalledAt: null,
+ },
+ create: {
+ slackTeamId: input.teamId,
+ ...(input.teamName ? { name: input.teamName } : {}),
+ ...(input.botUserId ? { botUserId: input.botUserId } : {}),
+ ...(input.installedBy ? { installedBySlackUserId: input.installedBy } : {}),
+ accessToken: this.encrypt(input.botToken),
+ },
+ });
+ }
+
+ async getBotToken(teamId: string): Promise {
+ const workspace = await this.db.workspace.findUnique({
+ where: { slackTeamId: teamId },
+ select: { accessToken: true, uninstalledAt: true },
+ });
+ if (!workspace?.accessToken || workspace.uninstalledAt) return null;
+ return this.decrypt(workspace.accessToken);
+ }
+
+ async getBotTokenByWorkspaceId(workspaceId: string): Promise {
+ const workspace = await this.db.workspace.findUnique({
+ where: { id: workspaceId },
+ select: { accessToken: true, uninstalledAt: true },
+ });
+ if (!workspace?.accessToken || workspace.uninstalledAt) return null;
+ return this.decrypt(workspace.accessToken);
+ }
+
+ async getInstallation(teamId: string): Promise {
+ const workspace = await this.db.workspace.findUnique({
+ where: { slackTeamId: teamId },
+ select: {
+ slackTeamId: true,
+ name: true,
+ botUserId: true,
+ installedBySlackUserId: true,
+ accessToken: true,
+ uninstalledAt: true,
+ },
+ });
+ if (!workspace?.accessToken || workspace.uninstalledAt) return null;
+ return {
+ teamId: workspace.slackTeamId,
+ ...(workspace.name ? { teamName: workspace.name } : {}),
+ ...(workspace.botUserId ? { botUserId: workspace.botUserId } : {}),
+ ...(workspace.installedBySlackUserId
+ ? { installedBy: workspace.installedBySlackUserId }
+ : {}),
+ botToken: this.decrypt(workspace.accessToken),
+ };
+ }
+
+ async markUninstalled(teamId: string): Promise {
+ const workspace = await this.db.workspace.findUnique({
+ where: { slackTeamId: teamId },
+ select: { id: true },
+ });
+ if (!workspace) return;
+ await this.db.$transaction([
+ this.db.workspace.update({
+ where: { id: workspace.id },
+ data: { uninstalledAt: new Date(), accessToken: null },
+ }),
+ this.db.reminder.updateMany({
+ where: {
+ event: { workspaceId: workspace.id },
+ status: ReminderStatus.PENDING,
+ },
+ data: { status: ReminderStatus.CANCELLED },
+ }),
+ ]);
+ }
+
+ private encrypt(value: string): string {
+ if (!this.encryptionKey) return value;
+ const key = Buffer.from(this.encryptionKey, "base64");
+ if (key.length !== 32) {
+ throw new Error("TOKEN_ENCRYPTION_KEY must be a base64-encoded 32-byte key.");
+ }
+ const iv = randomBytes(12);
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
+ const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
+ return [
+ "v1",
+ iv.toString("base64"),
+ cipher.getAuthTag().toString("base64"),
+ encrypted.toString("base64"),
+ ].join(".");
+ }
+
+ private decrypt(value: string): string {
+ if (!value.startsWith("v1.")) return value;
+ if (!this.encryptionKey) {
+ throw new Error("TOKEN_ENCRYPTION_KEY is required to decrypt OAuth tokens.");
+ }
+ const [, ivValue, tagValue, encryptedValue] = value.split(".");
+ if (!ivValue || !tagValue || !encryptedValue)
+ throw new Error("Invalid token payload.");
+ const key = Buffer.from(this.encryptionKey, "base64");
+ const decipher = createDecipheriv(
+ "aes-256-gcm",
+ key,
+ Buffer.from(ivValue, "base64"),
+ );
+ decipher.setAuthTag(Buffer.from(tagValue, "base64"));
+ return Buffer.concat([
+ decipher.update(Buffer.from(encryptedValue, "base64")),
+ decipher.final(),
+ ]).toString("utf8");
+ }
+}
diff --git a/src/tests/blocks.test.ts b/src/tests/blocks.test.ts
new file mode 100644
index 0000000..1732b95
--- /dev/null
+++ b/src/tests/blocks.test.ts
@@ -0,0 +1,63 @@
+import { EventStatus, EventType } from "@prisma/client";
+import { describe, expect, it } from "vitest";
+import { renderEventCard } from "../blocks/event-card.blocks.js";
+import { eventFixture } from "./fixtures.js";
+
+const text = (blocks: ReturnType) => JSON.stringify(blocks);
+
+describe("event card blocks", () => {
+ it("renders a lunch card", () => {
+ expect(text(renderEventCard(eventFixture()))).toContain("Lunch details");
+ expect(text(renderEventCard(eventFixture()))).toContain("$15");
+ });
+
+ it("renders a potluck card with claimed and needed items", () => {
+ const event = eventFixture({
+ type: EventType.POTLUCK,
+ metadata: { neededCategories: ["Main dish", "Dessert"] },
+ potluckItems: [
+ {
+ id: "item_1",
+ eventId: "event_1",
+ category: "Dessert",
+ itemName: "Brownies",
+ claimedBySlackUserId: "U1",
+ servesCount: 8,
+ dietaryType: "Veg",
+ notes: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ ],
+ });
+ const output = text(renderEventCard(event));
+ expect(output).toContain("Brownies");
+ expect(output).toContain("Main dish");
+ });
+
+ it("renders a weekend run card", () => {
+ const output = text(
+ renderEventCard(
+ eventFixture({
+ type: EventType.WEEKEND_RUN,
+ metadata: {
+ distance: "10K",
+ pace: "Easy",
+ beginnerFriendly: true,
+ },
+ }),
+ ),
+ );
+ expect(output).toContain("10K");
+ expect(output).toContain("Beginner-friendly");
+ });
+
+ it("renders cancelled and closed states", () => {
+ expect(
+ text(renderEventCard(eventFixture({ status: EventStatus.CANCELLED }))),
+ ).toContain("cancelled");
+ expect(
+ text(renderEventCard(eventFixture({ status: EventStatus.RSVP_CLOSED }))),
+ ).toContain("RSVPs are closed");
+ });
+});
diff --git a/src/tests/env.test.ts b/src/tests/env.test.ts
new file mode 100644
index 0000000..277bfed
--- /dev/null
+++ b/src/tests/env.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it } from "vitest";
+import { loadEnv } from "../config/env.js";
+
+const base = {
+ DATABASE_URL: "postgresql://teamloop:teamloop@localhost:5432/teamloop",
+ SLACK_SIGNING_SECRET: "signing-secret",
+};
+
+describe("environment configuration", () => {
+ it("accepts blank optional OAuth fields in local bot-token mode", () => {
+ const env = loadEnv({
+ ...base,
+ SLACK_BOT_TOKEN: "xoxb-test",
+ SLACK_CLIENT_ID: "",
+ SLACK_CLIENT_SECRET: "",
+ SLACK_STATE_SECRET: "",
+ TOKEN_ENCRYPTION_KEY: "",
+ });
+ expect(env.SLACK_CLIENT_ID).toBeUndefined();
+ });
+
+ it("requires token encryption for OAuth mode", () => {
+ expect(() =>
+ loadEnv({
+ ...base,
+ SLACK_CLIENT_ID: "client-id",
+ SLACK_CLIENT_SECRET: "client-secret",
+ SLACK_STATE_SECRET: "a".repeat(32),
+ }),
+ ).toThrow("TOKEN_ENCRYPTION_KEY");
+ });
+});
diff --git a/src/tests/event.service.test.ts b/src/tests/event.service.test.ts
new file mode 100644
index 0000000..39f42e0
--- /dev/null
+++ b/src/tests/event.service.test.ts
@@ -0,0 +1,84 @@
+import { EventStatus, EventType, type PrismaClient } from "@prisma/client";
+import { describe, expect, it, vi } from "vitest";
+import { EventService } from "../services/event.service.js";
+import { eventFixture } from "./fixtures.js";
+
+function database() {
+ const tx = {
+ workspace: {
+ upsert: vi.fn().mockResolvedValue({ id: "workspace_1" }),
+ },
+ channel: {
+ upsert: vi.fn().mockResolvedValue({ id: "channel_1" }),
+ },
+ event: {
+ create: vi.fn().mockResolvedValue({ id: "event_1" }),
+ update: vi.fn().mockResolvedValue({}),
+ findUniqueOrThrow: vi.fn().mockResolvedValue(eventFixture()),
+ },
+ potluckItem: { createMany: vi.fn().mockResolvedValue({ count: 0 }) },
+ auditLog: { create: vi.fn().mockResolvedValue({}) },
+ reminder: { updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
+ };
+ const db = {
+ ...tx,
+ event: {
+ ...tx.event,
+ findUnique: vi.fn().mockResolvedValue(eventFixture()),
+ },
+ $transaction: vi.fn(async (callback: (client: typeof tx) => unknown) =>
+ callback(tx),
+ ),
+ } as unknown as PrismaClient;
+ return { db, tx };
+}
+
+describe("EventService", () => {
+ it("creates an event and audit record", async () => {
+ const { db, tx } = database();
+ const event = await new EventService(db).create({
+ slackTeamId: "T1",
+ slackChannelId: "C1",
+ organizerSlackUserId: "U_ORGANIZER",
+ type: EventType.LUNCH,
+ title: "Lunch",
+ location: "Cafe",
+ eventStartsAt: new Date("2030-01-01T12:00:00Z"),
+ });
+ expect(event.id).toBe("event_1");
+ expect(tx.event.create).toHaveBeenCalledOnce();
+ expect(tx.auditLog.create).toHaveBeenCalledOnce();
+ });
+
+ it("updates an organizer-owned event", async () => {
+ const { db, tx } = database();
+ await new EventService(db).update("event_1", "U_ORGANIZER", {
+ title: "Updated lunch",
+ });
+ expect(tx.event.update).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: expect.objectContaining({ title: "Updated lunch" }),
+ }),
+ );
+ });
+
+ it("rejects invalid organizer actions", async () => {
+ const { db } = database();
+ await expect(
+ new EventService(db).cancel("event_1", "U_SOMEONE_ELSE"),
+ ).rejects.toThrow("Only the organizer");
+ });
+
+ it("closes RSVP and cancels events without deleting them", async () => {
+ const { db, tx } = database();
+ const service = new EventService(db);
+ await service.closeRsvp("event_1", "U_ORGANIZER");
+ expect(tx.event.update).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: expect.objectContaining({ status: EventStatus.RSVP_CLOSED }),
+ }),
+ );
+ await service.cancel("event_1", "U_ORGANIZER");
+ expect(tx.reminder.updateMany).toHaveBeenCalledOnce();
+ });
+});
diff --git a/src/tests/fixtures.ts b/src/tests/fixtures.ts
new file mode 100644
index 0000000..6e3a8c4
--- /dev/null
+++ b/src/tests/fixtures.ts
@@ -0,0 +1,31 @@
+import { EventStatus, EventType } from "@prisma/client";
+import type { EventWithRelations } from "../types/event.types.js";
+
+export function eventFixture(
+ overrides: Partial = {},
+): EventWithRelations {
+ const base: EventWithRelations = {
+ id: "event_1",
+ workspaceId: "workspace_1",
+ channelId: "channel_1",
+ organizerSlackUserId: "U_ORGANIZER",
+ type: EventType.LUNCH,
+ title: "Team Lunch",
+ description: "Meet in the lobby.",
+ location: "Green Cafe",
+ eventStartsAt: new Date("2030-06-22T12:30:00.000Z"),
+ rsvpDeadlineAt: new Date("2030-06-22T10:00:00.000Z"),
+ status: EventStatus.OPEN,
+ slackMessageTs: "123.456",
+ slackChannelId: "C_TEAM",
+ metadata: { budget: "$15", foodPreferencesEnabled: true },
+ createdAt: new Date("2030-06-20T00:00:00.000Z"),
+ updatedAt: new Date("2030-06-20T00:00:00.000Z"),
+ cancelledAt: null,
+ closedAt: null,
+ rsvps: [],
+ preferences: [],
+ potluckItems: [],
+ };
+ return { ...base, ...overrides };
+}
diff --git a/src/tests/reminder.service.test.ts b/src/tests/reminder.service.test.ts
new file mode 100644
index 0000000..a93105a
--- /dev/null
+++ b/src/tests/reminder.service.test.ts
@@ -0,0 +1,56 @@
+import { ReminderStatus, ReminderType, type PrismaClient } from "@prisma/client";
+import { describe, expect, it, vi } from "vitest";
+import { ReminderService } from "../services/reminder.service.js";
+
+function database() {
+ return {
+ reminder: {
+ upsert: vi.fn().mockResolvedValue({}),
+ create: vi.fn().mockResolvedValue({ id: "manual_1" }),
+ findMany: vi.fn().mockResolvedValue([{ id: "due_1" }]),
+ updateMany: vi.fn().mockResolvedValue({ count: 1 }),
+ update: vi.fn().mockResolvedValue({}),
+ },
+ } as unknown as PrismaClient;
+}
+
+describe("ReminderService", () => {
+ it("creates default reminders idempotently", async () => {
+ const db = database();
+ await new ReminderService(db).createDefaults({
+ eventId: "event_1",
+ eventStartsAt: new Date(Date.now() + 48 * 60 * 60 * 1000),
+ rsvpDeadlineAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
+ eventReminderMinutes: 60,
+ });
+ expect(db.reminder.upsert).toHaveBeenCalledTimes(2);
+ });
+
+ it("fetches and atomically claims due reminders", async () => {
+ const db = database();
+ const service = new ReminderService(db);
+ await expect(service.due()).resolves.toEqual([{ id: "due_1" }]);
+ await expect(service.claim("due_1")).resolves.toBe(true);
+ expect(db.reminder.updateMany).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: { id: "due_1", status: ReminderStatus.PENDING },
+ }),
+ );
+ });
+
+ it("marks reminders sent or failed", async () => {
+ const db = database();
+ const service = new ReminderService(db);
+ await service.markSent("due_1");
+ await service.markFailed("due_2", new Error("boom"));
+ expect(db.reminder.update).toHaveBeenCalledTimes(2);
+ });
+
+ it("creates a manual reminder", async () => {
+ const db = database();
+ await new ReminderService(db).createManual("event_1");
+ expect(db.reminder.create).toHaveBeenCalledWith({
+ data: expect.objectContaining({ type: ReminderType.MANUAL }),
+ });
+ });
+});
diff --git a/src/tests/rsvp.service.test.ts b/src/tests/rsvp.service.test.ts
new file mode 100644
index 0000000..03ed774
--- /dev/null
+++ b/src/tests/rsvp.service.test.ts
@@ -0,0 +1,48 @@
+import { EventStatus, RsvpStatus, type PrismaClient } from "@prisma/client";
+import { describe, expect, it, vi } from "vitest";
+import { RsvpService } from "../services/rsvp.service.js";
+
+function database(status: EventStatus) {
+ return {
+ event: { findUnique: vi.fn().mockResolvedValue({ status }) },
+ rSVP: {
+ upsert: vi.fn().mockResolvedValue({ id: "rsvp_1" }),
+ groupBy: vi.fn().mockResolvedValue([
+ { status: RsvpStatus.GOING, _count: { _all: 2 } },
+ { status: RsvpStatus.MAYBE, _count: { _all: 1 } },
+ ]),
+ },
+ } as unknown as PrismaClient;
+}
+
+describe("RsvpService", () => {
+ it("creates or updates one RSVP per user", async () => {
+ const db = database(EventStatus.OPEN);
+ const service = new RsvpService(db);
+ await service.upsert("event_1", "U1", RsvpStatus.GOING);
+ expect(db.rSVP.upsert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: { eventId_slackUserId: { eventId: "event_1", slackUserId: "U1" } },
+ }),
+ );
+ });
+
+ it.each([
+ [EventStatus.RSVP_CLOSED, "RSVPs are closed"],
+ [EventStatus.CANCELLED, "cancelled"],
+ ])("prevents RSVP when event is %s", async (status, message) => {
+ await expect(
+ new RsvpService(database(status)).upsert("event_1", "U1", RsvpStatus.GOING),
+ ).rejects.toThrow(message);
+ });
+
+ it("counts RSVP statuses", async () => {
+ await expect(
+ new RsvpService(database(EventStatus.OPEN)).counts("event_1"),
+ ).resolves.toEqual({
+ going: 2,
+ maybe: 1,
+ cannotMakeIt: 0,
+ });
+ });
+});
diff --git a/src/tests/validation.test.ts b/src/tests/validation.test.ts
new file mode 100644
index 0000000..2989b3e
--- /dev/null
+++ b/src/tests/validation.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it, vi } from "vitest";
+import { eventInputSchema } from "../utils/validation.js";
+
+describe("event validation", () => {
+ it("accepts a future event with an earlier RSVP deadline", () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2030-01-01T00:00:00.000Z"));
+ const result = eventInputSchema.safeParse({
+ title: "Team Lunch",
+ date: "2030-01-02",
+ time: "12:00",
+ location: "Cafe",
+ rsvpDeadlineDate: "2030-01-02",
+ rsvpDeadlineTime: "10:00",
+ });
+ expect(result.success).toBe(true);
+ vi.useRealTimers();
+ });
+
+ it("rejects a past event", () => {
+ const result = eventInputSchema.safeParse({
+ title: "Team Lunch",
+ date: "2020-01-02",
+ time: "12:00",
+ location: "Cafe",
+ rsvpDeadlineDate: "2020-01-02",
+ rsvpDeadlineTime: "10:00",
+ });
+ expect(result.success).toBe(false);
+ });
+
+ it("rejects an RSVP deadline after the event", () => {
+ const result = eventInputSchema.safeParse({
+ title: "Team Lunch",
+ date: "2035-01-02",
+ time: "12:00",
+ location: "Cafe",
+ rsvpDeadlineDate: "2035-01-02",
+ rsvpDeadlineTime: "13:00",
+ });
+ expect(result.success).toBe(false);
+ });
+});
diff --git a/src/types/event.types.ts b/src/types/event.types.ts
new file mode 100644
index 0000000..c540627
--- /dev/null
+++ b/src/types/event.types.ts
@@ -0,0 +1,38 @@
+import type { Event, EventType, PotluckItem, Preference, RSVP } from "@prisma/client";
+import type { Prisma } from "@prisma/client";
+
+export type EventMetadata = Record;
+
+export interface CreateEventInput {
+ slackTeamId: string;
+ slackTeamName?: string;
+ slackChannelId: string;
+ slackChannelName?: string;
+ organizerSlackUserId: string;
+ type: EventType;
+ title: string;
+ description?: string;
+ location: string;
+ eventStartsAt: Date;
+ rsvpDeadlineAt?: Date;
+ metadata?: EventMetadata;
+}
+
+export interface UpdateEventInput {
+ title?: string;
+ description?: string | null;
+ location?: string;
+ eventStartsAt?: Date;
+ rsvpDeadlineAt?: Date | null;
+ metadata?: EventMetadata;
+}
+
+export type EventWithRelations = Event & {
+ rsvps: RSVP[];
+ preferences: Preference[];
+ potluckItems: PotluckItem[];
+};
+
+export function metadataToJson(metadata: EventMetadata): Prisma.InputJsonValue {
+ return metadata;
+}
diff --git a/src/types/slack.types.ts b/src/types/slack.types.ts
new file mode 100644
index 0000000..3546331
--- /dev/null
+++ b/src/types/slack.types.ts
@@ -0,0 +1,17 @@
+import type { ViewStateValue } from "@slack/bolt";
+
+export interface CommandMetadata {
+ channelId: string;
+ channelName?: string;
+ userId: string;
+ teamId: string;
+ teamName?: string;
+ eventType: string;
+}
+
+export type SlackViewValues = Record>;
+
+export interface EventActionValue {
+ eventId: string;
+ value?: string;
+}
diff --git a/src/utils/date.ts b/src/utils/date.ts
new file mode 100644
index 0000000..ae813a5
--- /dev/null
+++ b/src/utils/date.ts
@@ -0,0 +1,19 @@
+const SLACK_DATE_FORMAT = "{date_long_pretty} at {time}";
+
+export function toUtcDate(date: string, time: string): Date {
+ return new Date(`${date}T${time}:00.000Z`);
+}
+
+export function slackDate(date: Date): string {
+ const unix = Math.floor(date.getTime() / 1000);
+ return ``;
+}
+
+export function slackTime(date: Date): string {
+ const unix = Math.floor(date.getTime() / 1000);
+ return ``;
+}
+
+export function minutesBefore(date: Date, minutes: number): Date {
+ return new Date(date.getTime() - minutes * 60_000);
+}
diff --git a/src/utils/errors.ts b/src/utils/errors.ts
new file mode 100644
index 0000000..91eaab6
--- /dev/null
+++ b/src/utils/errors.ts
@@ -0,0 +1,32 @@
+export class TeamLoopError extends Error {
+ constructor(
+ message: string,
+ public readonly code: string,
+ ) {
+ super(message);
+ this.name = "TeamLoopError";
+ }
+}
+
+export class NotFoundError extends TeamLoopError {
+ constructor(message = "That event no longer exists.") {
+ super(message, "NOT_FOUND");
+ }
+}
+
+export class PermissionError extends TeamLoopError {
+ constructor(message = "Only the organizer can do that.") {
+ super(message, "FORBIDDEN");
+ }
+}
+
+export class EventStateError extends TeamLoopError {
+ constructor(message: string, code = "INVALID_EVENT_STATE") {
+ super(message, code);
+ }
+}
+
+export function toUserMessage(error: unknown): string {
+ if (error instanceof TeamLoopError) return error.message;
+ return "Something went wrong. Please try again.";
+}
diff --git a/src/utils/text.ts b/src/utils/text.ts
new file mode 100644
index 0000000..8d26ae1
--- /dev/null
+++ b/src/utils/text.ts
@@ -0,0 +1,16 @@
+export function escapeMrkdwn(value: string): string {
+ return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
+}
+
+export function truncate(value: string, maxLength: number): string {
+ if (value.length <= maxLength) return value;
+ return `${value.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
+}
+
+export function titleCase(value: string): string {
+ return value
+ .toLowerCase()
+ .split("_")
+ .map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
+ .join(" ");
+}
diff --git a/src/utils/validation.ts b/src/utils/validation.ts
new file mode 100644
index 0000000..433dc1a
--- /dev/null
+++ b/src/utils/validation.ts
@@ -0,0 +1,46 @@
+import { z } from "zod";
+import { toUtcDate } from "./date.js";
+
+export const eventInputSchema = z
+ .object({
+ title: z.string().trim().min(1, "Activity title is required.").max(150),
+ date: z.string().date(),
+ time: z.string().regex(/^\d{2}:\d{2}$/),
+ location: z.string().trim().min(1, "Location is required.").max(300),
+ rsvpDeadlineDate: z.string().date(),
+ rsvpDeadlineTime: z.string().regex(/^\d{2}:\d{2}$/),
+ notes: z.string().trim().max(1_500).optional(),
+ reminderMinutes: z.coerce.number().int().min(0).max(10_080).optional(),
+ })
+ .transform((value) => ({
+ ...value,
+ eventStartsAt: toUtcDate(value.date, value.time),
+ rsvpDeadlineAt: toUtcDate(value.rsvpDeadlineDate, value.rsvpDeadlineTime),
+ }))
+ .superRefine((value, context) => {
+ if (value.eventStartsAt <= new Date()) {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["date"],
+ message: "Please choose a future date and time.",
+ });
+ }
+ if (value.rsvpDeadlineAt >= value.eventStartsAt) {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["rsvpDeadlineDate"],
+ message: "RSVP deadline must be before the event starts.",
+ });
+ }
+ });
+
+export type ValidatedEventInput = z.infer;
+
+export function formatZodErrors(error: z.ZodError): Record {
+ const result: Record = {};
+ for (const issue of error.issues) {
+ const key = issue.path[0];
+ if (typeof key === "string" && !result[key]) result[key] = issue.message;
+ }
+ return result;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..bc2bc2a
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2022"],
+ "outDir": "dist",
+ "rootDir": ".",
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "types": ["node", "vitest/globals"]
+ },
+ "include": ["src/**/*.ts", "prisma/**/*.ts", "prisma.config.ts", "vitest.config.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..83dc7a4
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ environment: "node",
+ include: ["src/tests/**/*.test.ts"],
+ coverage: {
+ reporter: ["text", "html"],
+ },
+ },
+});