From 04f42ff419b902e3af344bf9898ff7fcabf6d0c4 Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:12:16 -0500 Subject: [PATCH] feat: add DM support by removing spaceId from query logic Since channelId is globally unique in Towns, spaceId was redundant for subscription queries. This change: - Makes spaceId nullable in database schema (oauth_states, github_subscriptions, pending_subscriptions) - Updates unique indexes to use only channelId + repoFullName - Removes spaceId from function signatures and WHERE clauses - Simplifies service calls throughout the codebase DMs now work because spaceId can be null without breaking queries. Space subscriptions continue to store spaceId for reference. Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 68 +- CONTRIBUTING.md | 25 +- README.md | 17 +- bun.lock | 99 +- drizzle/0009_nice_eddie_brock.sql | 10 + drizzle/meta/0009_snapshot.json | 911 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 23 +- src/db/schema.ts | 14 +- src/handlers/github-subscription-handler.ts | 23 +- src/routes/oauth-callback.ts | 15 +- src/services/github-oauth-service.ts | 10 +- src/services/message-delivery-service.ts | 29 +- src/services/subscription-service.ts | 60 +- src/utils/oauth-helpers.ts | 6 +- .../github-subscription-handler.test.ts | 7 +- 16 files changed, 1112 insertions(+), 212 deletions(-) create mode 100644 drizzle/0009_nice_eddie_brock.sql create mode 100644 drizzle/meta/0009_snapshot.json diff --git a/AGENTS.md b/AGENTS.md index e6a9465..b3dc098 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,16 +14,44 @@ This file provides guidance to AI agents for building Towns Protocol bots. ## Base Payload -All events include: +All events use a discriminated union based on `isDm`: ```typescript -{ - userId: string; // Hex address (0x...) - spaceId: string; - channelId: string; - eventId: string; // Unique event ID (use as threadId/replyId when responding) - createdAt: Date; -} +type BasePayload = + | { + userId: string; // Hex address (0x...) + spaceId: null; // Always null for DM channels + channelId: string; + eventId: string; // Unique event ID (use as threadId/replyId when responding) + createdAt: Date; + event: StreamEvent; + isDm: true; // Discriminator: when true, spaceId is null + } + | { + userId: string; // Hex address (0x...) + spaceId: string; // Always a string for space channels + channelId: string; + eventId: string; // Unique event ID (use as threadId/replyId when responding) + createdAt: Date; + event: StreamEvent; + isDm: false; // Discriminator: when false, spaceId is string + }; +``` + +**Type Safety:** TypeScript automatically narrows the type based on `isDm`. When `isDm` is `true`, `spaceId` is guaranteed to be `null`. When `isDm` is `false`, `spaceId` is guaranteed to be a `string`. + +**Example:** + +```typescript +bot.onMessage(async (handler, event) => { + if (event.isDm) { + // TypeScript knows spaceId is null here + console.log("DM channel, no space"); + } else { + // TypeScript knows spaceId is string here + console.log(`Space: ${event.spaceId}`); + } +}); ``` ## Event Handlers @@ -82,6 +110,14 @@ bot.onSlashCommand("help", async (handler, event) => { }); ``` +#### Paid Commands + +Add a `paid` property to your command definition with a price in USDC: + +```typescript +{ name: "generate", description: "Generate AI content", paid: { price: '$0.20' } } +``` + ### onReaction **When:** User adds emoji reaction @@ -322,6 +358,8 @@ const hash = await execute(bot.viem, { ## External Interactions (Unprompted Messages) +`bot.start()` returns a **Hono app**. To extend with additional routes, create a new Hono app and use `.route('/', app)` per https://hono.dev/docs/guides/best-practices#building-a-larger-application + **All handler methods available on bot** (webhooks, timers, tasks): You need data prior (channelId, spaceId, etc): @@ -332,20 +370,6 @@ bot.hasAdminPermission(...) | bot.checkPermission(...) | bot.ban(...) | bot.unba // Properties: bot.botId, bot.viem, bot.appAddress ``` -**GitHub Integration Pattern:** - -```typescript -let channelId = null; -bot.onSlashCommand("setup", async (h, e) => { - channelId = e.channelId; -}); -app.post("/webhook", bot.start().jwtMiddleware, bot.start().handler); -app.post("/github", async c => { - if (channelId) await bot.sendMessage(channelId, `PR: ${c.req.json().title}`); - return c.json({ ok: true }); -}); -``` - **Patterns:** Store channel IDs | Webhooks/timers | Call bot.\* directly | Handle errors ## Critical Notes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79f4d31..b87ba82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,13 +24,8 @@ Thank you for your interest in contributing! This guide covers development setup 2. **Setup database** ```bash - # Start PostgreSQL with Docker - docker run --rm --name github-bot-db \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=postgres \ - -e POSTGRES_DB=github-bot \ - -p 5432:5432 \ - postgres:18 + bun db:up # Start PostgreSQL (data persists across restarts) + bun db:migrate # Run migrations ``` 3. **Configure environment** (see Configuration section below) @@ -65,14 +60,14 @@ Thank you for your interest in contributing! This guide covers development setup #### GitHub App (Optional - Enables Real-Time Webhooks) -| Variable | Description | How to Get | -|---------------------------------| ------------------------------------------ | ---------------------------------------------------------- | -| `GITHUB_APP_ID` | GitHub App ID | GitHub App settings page | -| `GITHUB_APP_PRIVATE_KEY_BASE64` | Base64-encoded private key | Download `.pem`, encode: `base64 -i key.pem | tr -d '\n'` | -| `GITHUB_APP_CLIENT_ID` | OAuth client ID (format: `Iv1.abc123`) | GitHub App OAuth settings | -| `GITHUB_APP_CLIENT_SECRET` | OAuth client secret | GitHub App OAuth settings | -| `GITHUB_WEBHOOK_SECRET` | Webhook signature secret | Generate: `openssl rand -hex 32` | -| `GITHUB_APP_SLUG` | App URL slug (default: `towns-github-bot`) | Optional - for custom app names | +| Variable | Description | How to Get | +| ------------------------------- | ------------------------------------------ | ------------------------------------------- | ----------- | +| `GITHUB_APP_ID` | GitHub App ID | GitHub App settings page | +| `GITHUB_APP_PRIVATE_KEY_BASE64` | Base64-encoded private key | Download `.pem`, encode: `base64 -i key.pem | tr -d '\n'` | +| `GITHUB_APP_CLIENT_ID` | OAuth client ID (format: `Iv1.abc123`) | GitHub App OAuth settings | +| `GITHUB_APP_CLIENT_SECRET` | OAuth client secret | GitHub App OAuth settings | +| `GITHUB_WEBHOOK_SECRET` | Webhook signature secret | Generate: `openssl rand -hex 32` | +| `GITHUB_APP_SLUG` | App URL slug (default: `towns-github-bot`) | Optional - for custom app names | #### Optional Configuration diff --git a/README.md b/README.md index 61c1cc5..34d7356 100644 --- a/README.md +++ b/README.md @@ -161,15 +161,18 @@ All webhook events above except `workflow_run` (CI/CD) are also available via po 3. **Start Postgres locally for development** - You can run Postgres in Docker with a single command: + Start the database with: ```bash - docker run --rm --name github-bot-db \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=postgres \ - -e POSTGRES_DB=github-bot \ - -p 5432:5432 \ - postgres:18 + bun db:up + ``` + + This creates a container with a named volume so your data persists across restarts. + + To stop the database: + + ```bash + bun db:down ``` Then point your `.env` at the container: diff --git a/bun.lock b/bun.lock index d1d857f..9809231 100644 --- a/bun.lock +++ b/bun.lock @@ -8,14 +8,13 @@ "@octokit/app": "^16.1.2", "@octokit/rest": "^22.0.1", "@octokit/webhooks": "^14.2.0", - "@towns-protocol/bot": "^0.0.453", - "@towns-protocol/proto": "^0.0.453", - "@towns-protocol/web3": "^0.0.453", + "@towns-protocol/bot": "^1.0.3", + "@towns-protocol/proto": "^1.0.3", "drizzle-orm": "^0.45.1", - "hono": "^4.11.3", + "hono": "^4.11.4", "picomatch": "^4.0.3", "postgres": "^3.4.8", - "viem": "^2.43.5", + "viem": "^2.44.2", "zod": "^4.3.5", }, "devDependencies": { @@ -23,22 +22,22 @@ "@eslint/js": "^9.39.2", "@ianvs/prettier-plugin-sort-imports": "^4.7.0", "@prettier/plugin-oxc": "^0.1.3", - "@types/bun": "^1.3.5", + "@types/bun": "^1.3.6", "@types/picomatch": "^4.0.2", - "@typescript-eslint/eslint-plugin": "^8.52.0", - "@typescript-eslint/parser": "^8.52.0", + "@typescript-eslint/eslint-plugin": "^8.53.0", + "@typescript-eslint/parser": "^8.53.0", "drizzle-kit": "^0.31.8", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-formatter-unix": "^9.0.1", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import-x": "^4.16.1", - "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-tsdoc": "^0.5.0", "prettier": "^3.7.4", "prettier-plugin-ember-template-tag": "^2.1.2", "typescript": "~5.8.3", - "typescript-eslint": "^8.52.0", + "typescript-eslint": "^8.53.0", }, }, }, @@ -537,31 +536,31 @@ "@tanstack/react-query": ["@tanstack/react-query@5.90.16", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="], - "@towns-protocol/bot": ["@towns-protocol/bot@0.0.453", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@connectrpc/connect-node": "^2.1.0", "@standard-schema/spec": "^1.0.0-beta.9", "@towns-protocol/encryption": "^0.0.453", "@towns-protocol/generated": "^0.0.453", "@towns-protocol/proto": "^0.0.453", "@towns-protocol/sdk": "^0.0.453", "@towns-protocol/sdk-crypto": "^0.0.453", "@towns-protocol/utils": "^0.0.453", "@towns-protocol/web3": "^0.0.453", "ethers": "^5.8.0", "image-size": "^2.0.2", "jsonwebtoken": "^9.0.2", "nanoevents": "^9.1.0", "superjson": "^2.2.2", "x402": "^0.7.3" }, "peerDependencies": { "hono": ">=4", "viem": "2.x" } }, "sha512-MB5qar9JjjKF9o346Q2W80Y1/EWoFvXgNx2ZacAtVQmY4ag0WHAvZ4vtr5yDDCmTUGIE1z5mFLV0KxNzaSHhLg=="], + "@towns-protocol/bot": ["@towns-protocol/bot@1.0.3", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@connectrpc/connect-node": "^2.1.0", "@standard-schema/spec": "^1.0.0-beta.9", "@towns-protocol/encryption": "^0.0.455", "@towns-protocol/generated": "^0.0.455", "@towns-protocol/proto": "^0.0.455", "@towns-protocol/sdk": "^0.0.455", "@towns-protocol/sdk-crypto": "^0.0.455", "@towns-protocol/utils": "^0.0.455", "@towns-protocol/web3": "^0.0.455", "ethers": "^5.8.0", "image-size": "^2.0.2", "jsonwebtoken": "^9.0.2", "nanoevents": "^9.1.0", "superjson": "^2.2.2", "x402": "^0.7.3" }, "peerDependencies": { "hono": ">=4", "viem": "2.x" } }, "sha512-YQLnOUI9t8hqBUqRk4deZufyThlT8vClON5NJY8Wqjc9x6QzK06qS0aQaq16AVpBlMjTpjNQfQ+ChJsKR/XFdA=="], - "@towns-protocol/encryption": ["@towns-protocol/encryption@0.0.453", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@towns-protocol/olm": "3.2.28", "@towns-protocol/proto": "^0.0.453", "@towns-protocol/utils": "^0.0.453", "@towns-protocol/web3": "^0.0.453", "debug": "^4.3.4", "dexie": "^4.2.1", "ethers": "^5.8.0", "lru-cache": "^11.0.1", "nanoid": "^4.0.0", "typed-emitter": "^2.1.0" } }, "sha512-LdSfCPTjE8ZzDoL/HYOextc0nNZFhMo2Fkq1DrGKK72cYGzhZe1tunYjTA6oZHPpn8bJrGR5vfBIRItjCU0RQQ=="], + "@towns-protocol/encryption": ["@towns-protocol/encryption@0.0.455", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@towns-protocol/olm": "3.2.28", "@towns-protocol/proto": "^0.0.455", "@towns-protocol/utils": "^0.0.455", "@towns-protocol/web3": "^0.0.455", "debug": "^4.3.4", "dexie": "^4.2.1", "ethers": "^5.8.0", "lru-cache": "^11.0.1", "nanoid": "^4.0.0", "typed-emitter": "^2.1.0" } }, "sha512-APhbIX5JyynjprlTu3HW5GEtKkmCmRCcvuPvAgKIHmo6Ra7WijMki35U0qWOe2jcteKkLVELpczBei2+cIAnXg=="], - "@towns-protocol/generated": ["@towns-protocol/generated@0.0.453", "", { "dependencies": { "@ethersproject/abi": "^5.8.0", "@ethersproject/providers": "^5.8.0", "ethers": "^5.8.0" } }, "sha512-jS6TRlYSkPJRy2u9uE0cpIoLLGiiI932DEWBXv3HPgRGhN7qnIbY0iPN4EzeUtHtKGnvsiIxVpFIEtuUAy4BUw=="], + "@towns-protocol/generated": ["@towns-protocol/generated@0.0.455", "", { "dependencies": { "@ethersproject/abi": "^5.8.0", "@ethersproject/providers": "^5.8.0", "ethers": "^5.8.0" } }, "sha512-/ne7AQmKOQTuJQtD/RjhEDfGJPN6Bhpm7sBpVzOlt8vH7YkoQqhULB7LfeeLHWNNBXZVc5pCQQxS7ipsQ0WXyw=="], "@towns-protocol/olm": ["@towns-protocol/olm@3.2.28", "", {}, "sha512-ezxkDYCm3wwqN957PyS6gfHqIrmA5FZkbeUePMxwcW5HkMImQGLhTZMVZvjnRJe/DjfxnqEP3T0Q99AshH02pw=="], - "@towns-protocol/proto": ["@towns-protocol/proto@0.0.453", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0" } }, "sha512-hFa6CB3XivlhhMs0dPM4zgsUJuvS9L4nudd1Vfgb6wbC3Fa6Ev7LazRy3lDUznY+KxesfEVigbZ1FZkFwVdR8g=="], + "@towns-protocol/proto": ["@towns-protocol/proto@1.0.3", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0" } }, "sha512-+MUpzHapRJ7+QlBZDLAxIPhKJAFUgoZ8kUEOhykSCiV5Qmp4mQIlIBtuVQ/EYZKV/7PBfCD7UoF7DCBp7o32HA=="], - "@towns-protocol/rpc-connector": ["@towns-protocol/rpc-connector@0.0.453", "", { "dependencies": { "@connectrpc/connect": "^2.1.0" }, "peerDependencies": { "@connectrpc/connect-node": ">=2.0.0", "@connectrpc/connect-web": ">=2.0.0" }, "optionalPeers": ["@connectrpc/connect-node", "@connectrpc/connect-web"] }, "sha512-NxWfjFjLI510oE2GoW+HMZ3HPQei4Nqyh2iNeqwf2ky0oEQmVA0t5lSt2da07eGRxoj+Iy9M0T+xFWtpRnv7eA=="], + "@towns-protocol/rpc-connector": ["@towns-protocol/rpc-connector@0.0.455", "", { "dependencies": { "@connectrpc/connect": "^2.1.0" }, "peerDependencies": { "@connectrpc/connect-node": ">=2.0.0", "@connectrpc/connect-web": ">=2.0.0" }, "optionalPeers": ["@connectrpc/connect-node", "@connectrpc/connect-web"] }, "sha512-GnAk3mnTs5IFiiLwMYsOYQ5VAwbchGypsS/RBx3MGrCKF/Xjk6z3xsTAEA6jB8NjZyuQqp7XaQydtWGAcavKzg=="], - "@towns-protocol/sdk": ["@towns-protocol/sdk@0.0.453", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@connectrpc/connect": "^2.1.0", "@ethereumjs/util": "^10.0.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0", "@towns-protocol/encryption": "^0.0.453", "@towns-protocol/generated": "^0.0.453", "@towns-protocol/proto": "^0.0.453", "@towns-protocol/rpc-connector": "^0.0.453", "@towns-protocol/sdk-crypto": "^0.0.453", "@towns-protocol/utils": "^0.0.453", "@towns-protocol/web3": "^0.0.453", "debug": "^4.3.4", "dexie": "^4.2.1", "ethereum-cryptography": "^3.2.0", "ethers": "^5.8.0", "lodash-es": "^4.17.21", "nanoid": "^4.0.0", "p-limit": "^6.1.0" } }, "sha512-fs+cOg4TlgML0dBMTKdrOoubO/Eh+rd9yRqOqEE4VW01IjG5sZ97ZlwriR9GpjrJUA0MQ3gsR3Abc7c8XdPxTQ=="], + "@towns-protocol/sdk": ["@towns-protocol/sdk@0.0.455", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@connectrpc/connect": "^2.1.0", "@ethereumjs/util": "^10.0.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0", "@towns-protocol/encryption": "^0.0.455", "@towns-protocol/generated": "^0.0.455", "@towns-protocol/proto": "^0.0.455", "@towns-protocol/rpc-connector": "^0.0.455", "@towns-protocol/sdk-crypto": "^0.0.455", "@towns-protocol/utils": "^0.0.455", "@towns-protocol/web3": "^0.0.455", "debug": "^4.3.4", "dexie": "^4.2.1", "ethereum-cryptography": "^3.2.0", "ethers": "^5.8.0", "lodash-es": "^4.17.21", "nanoid": "^4.0.0", "p-limit": "^6.1.0" } }, "sha512-6crGed2m2PSeeoPGCie+64OnFpi+pvYt0vEyZByvZrSoT/v4fop344kdaZ4da03W5qm7ppbjVsFC1fDh27H35A=="], - "@towns-protocol/sdk-crypto": ["@towns-protocol/sdk-crypto@0.0.453", "", { "dependencies": { "@towns-protocol/encryption": "^0.0.453", "@towns-protocol/proto": "^0.0.453", "@towns-protocol/utils": "^0.0.453" } }, "sha512-AX6X1DvjmtkwgXAGEHcHfEog/0TAbh/Uto/ZzlSlbPPSu3FHBuvFWcEneGuiQ8GihkBAtvlbU7EbWeMLglZ4Mw=="], + "@towns-protocol/sdk-crypto": ["@towns-protocol/sdk-crypto@0.0.455", "", { "dependencies": { "@towns-protocol/encryption": "^0.0.455", "@towns-protocol/proto": "^0.0.455", "@towns-protocol/utils": "^0.0.455" } }, "sha512-NXSirZaHW4IfdNs1P3v1qmM5hqlKtZEc+wqavQkTD7qDstYNVJS2AJb6h9rK4AI65XQxlEwYd9KFVZz+OtqhxQ=="], - "@towns-protocol/utils": ["@towns-protocol/utils@0.0.453", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@ethereumjs/util": "^10.0.0", "@noble/hashes": "^1.8.0", "@towns-protocol/proto": "^0.0.453", "debug": "^4.3.4", "ethereum-cryptography": "^3.2.0" } }, "sha512-94IPjx79Xb9OtFAFD9RIVrwkG/Fk4j3PIlBLIAgyGeCLUYMLMyTo+QWcMamcvo4BFlBo79pF87jIdcA+vj5dYA=="], + "@towns-protocol/utils": ["@towns-protocol/utils@0.0.455", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0", "@ethereumjs/util": "^10.0.0", "@noble/hashes": "^1.8.0", "@towns-protocol/proto": "^0.0.455", "debug": "^4.3.4", "ethereum-cryptography": "^3.2.0" } }, "sha512-pqblIgI6bGxmPCwj3HP+iQeoc/B/j2seIDNJArQ8QyILO+Yi/Dj1mN3C4lr8dw9UWotpG0eVVy6mnQ/eC9LP8Q=="], - "@towns-protocol/web3": ["@towns-protocol/web3@0.0.453", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@towns-protocol/generated": "^0.0.453", "@towns-protocol/utils": "^0.0.453", "abitype": "^0.9.10", "debug": "^4.3.4", "ethers": "^5.8.0", "lru-cache": "^11.0.1", "nanoid": "^4.0.0", "viem": "^2.29.3", "zod": "^3.25.76" } }, "sha512-Igw7xWtZcLW89nxVII4QI5m/0pVefBT/tm4MmwHuApKg7eNi7/L3N96u80v0Y/muA7VQgqglMd4qnFGRkE+KiA=="], + "@towns-protocol/web3": ["@towns-protocol/web3@0.0.455", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@towns-protocol/generated": "^0.0.455", "@towns-protocol/utils": "^0.0.455", "abitype": "^0.9.10", "debug": "^4.3.4", "ethers": "^5.8.0", "lru-cache": "^11.0.1", "nanoid": "^4.0.0", "superjson": "^2.2.2", "viem": "^2.29.3", "zod": "^3.25.76" } }, "sha512-rs/sPRrV3h9nLFjQyD9T2GMjszYSPW5PIshkzNhwl+xVVMLrRcael3ycseYH1GgbetgmT8QbwqfsanknmUSq9Q=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/aws-lambda": ["@types/aws-lambda@8.10.158", "", {}, "sha512-v/n2WsL1ksRKigfqZ9ff7ANobfT3t/T8kI8UOiur98tREwFulv9lRv+pDrocGPWOe3DpD2Y2GKRO+OiyxwgaCQ=="], - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -585,25 +584,25 @@ "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.0", "@typescript-eslint/types": "^8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.0", "@typescript-eslint/tsconfig-utils": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw=="], "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], @@ -697,7 +696,7 @@ "@walletconnect/window-metadata": ["@walletconnect/window-metadata@1.0.1", "", { "dependencies": { "@walletconnect/window-getters": "^1.0.1", "tslib": "1.14.1" } }, "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA=="], - "abitype": ["abitype@0.9.10", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-FIS7U4n7qwAT58KibwYig5iFG4K61rbhAqaQh/UWj8v1Y8mjX3F8TC9gd8cz9yT1TYel9f8nS5NO5kZp2RW0jQ=="], + "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -767,7 +766,7 @@ "bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -915,7 +914,7 @@ "eslint-plugin-import-x": ["eslint-plugin-import-x@4.16.1", "", { "dependencies": { "@typescript-eslint/types": "^8.35.0", "comment-parser": "^1.4.1", "debug": "^4.4.1", "eslint-import-context": "^0.1.9", "is-glob": "^4.0.3", "minimatch": "^9.0.3 || ^10.0.1", "semver": "^7.7.2", "stable-hash-x": "^0.2.0", "unrs-resolver": "^1.9.2" }, "peerDependencies": { "@typescript-eslint/utils": "^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "eslint-import-resolver-node": "*" }, "optionalPeers": ["@typescript-eslint/utils", "eslint-import-resolver-node"] }, "sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ=="], - "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.4", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg=="], + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.12" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw=="], "eslint-plugin-tsdoc": ["eslint-plugin-tsdoc@0.5.0", "", { "dependencies": { "@microsoft/tsdoc": "0.16.0", "@microsoft/tsdoc-config": "0.18.0", "@typescript-eslint/utils": "~8.46.0" } }, "sha512-ush8ehCwub2rgE16OIgQPFyj/o0k3T8kL++9IrAI4knsmupNo8gvfO2ERgDHWWgTC5MglbwLVRswU93HyXqNpw=="], @@ -1033,7 +1032,7 @@ "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], - "hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="], + "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], @@ -1225,7 +1224,7 @@ "optionator": ["optionator@0.9.4", "", { "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" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "ox": ["ox@0.11.1", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1l1gOLAqg0S0xiN1dH5nkPna8PucrZgrIJOfS49MLNiMevxu07Iz4ZjuJS9N+xifvT+PsZyIptS7WHM8nC+0+A=="], + "ox": ["ox@0.11.3", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw=="], "oxc-parser": ["oxc-parser@0.99.0", "", { "dependencies": { "@oxc-project/types": "^0.99.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm64": "0.99.0", "@oxc-parser/binding-darwin-arm64": "0.99.0", "@oxc-parser/binding-darwin-x64": "0.99.0", "@oxc-parser/binding-freebsd-x64": "0.99.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.99.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.99.0", "@oxc-parser/binding-linux-arm64-gnu": "0.99.0", "@oxc-parser/binding-linux-arm64-musl": "0.99.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.99.0", "@oxc-parser/binding-linux-s390x-gnu": "0.99.0", "@oxc-parser/binding-linux-x64-gnu": "0.99.0", "@oxc-parser/binding-linux-x64-musl": "0.99.0", "@oxc-parser/binding-wasm32-wasi": "0.99.0", "@oxc-parser/binding-win32-arm64-msvc": "0.99.0", "@oxc-parser/binding-win32-x64-msvc": "0.99.0" } }, "sha512-MpS1lbd2vR0NZn1v0drpgu7RUFu3x9Rd0kxExObZc2+F+DIrV0BOMval/RO3BYGwssIOerII6iS8EbbpCCZQpQ=="], @@ -1271,7 +1270,7 @@ "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], - "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], "prettier-plugin-ember-template-tag": ["prettier-plugin-ember-template-tag@2.1.2", "", { "dependencies": { "@babel/core": "^7.27.4", "content-tag": "^4.0.0" }, "peerDependencies": { "prettier": ">= 3.0.0" } }, "sha512-SZqbv8qfqJ7AnAaNhyeHOzsq1kvlkHdVrCAZDVBjUeWuW8xF2AK7LfKRkn9MPEUsbEO7BSkLsjkb5++eoWjtGw=="], @@ -1385,7 +1384,7 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], + "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], @@ -1413,7 +1412,7 @@ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="], + "typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="], "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], @@ -1447,7 +1446,7 @@ "valtio": ["valtio@1.13.2", "", { "dependencies": { "derive-valtio": "0.1.0", "proxy-compare": "2.6.0", "use-sync-external-store": "1.2.0" }, "peerDependencies": { "@types/react": ">=16.8", "react": ">=16.8" }, "optionalPeers": ["@types/react", "react"] }, "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A=="], - "viem": ["viem@2.43.5", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.11.1", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-QuJpuEMEPM3EreN+vX4mVY68Sci0+zDxozYfbh/WfV+SSy/Gthm74PH8XmitXdty1xY54uTCJ+/Gbbd1IiMPSA=="], + "viem": ["viem@2.44.2", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.11.3", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-nHY872t/T3flLpVsnvQT/89bwbrJwxaL917FDv7Oxy4E5FWIFkokRQOKXG3P+hgl30QYVZxi9o2SUHLnebycxw=="], "wagmi": ["wagmi@2.19.5", "", { "dependencies": { "@wagmi/connectors": "6.2.0", "@wagmi/core": "2.22.1", "use-sync-external-store": "1.4.0" }, "peerDependencies": { "@tanstack/react-query": ">=5.0.0", "react": ">=18", "typescript": ">=5.0.4", "viem": "2.x" }, "optionalPeers": ["typescript"] }, "sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ=="], @@ -1591,9 +1590,21 @@ "@solana/web3.js/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + "@towns-protocol/bot/@towns-protocol/proto": ["@towns-protocol/proto@0.0.455", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0" } }, "sha512-jAc/w7HknqKEtBeh4Jj277ssg3TA70jv78bkBTIsyfU3WnCrTy0jl/pee181mptTbxQHylG+7mQxg077MHsPWw=="], + + "@towns-protocol/encryption/@towns-protocol/proto": ["@towns-protocol/proto@0.0.455", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0" } }, "sha512-jAc/w7HknqKEtBeh4Jj277ssg3TA70jv78bkBTIsyfU3WnCrTy0jl/pee181mptTbxQHylG+7mQxg077MHsPWw=="], + "@towns-protocol/sdk/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "@towns-protocol/web3/viem": ["viem@2.40.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.1.0", "isows": "1.0.7", "ox": "0.9.6", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-feYfEpbgjRkZYQpwcgxqkWzjxHI5LSDAjcGetHHwDRuX9BRQHUdV8ohrCosCYpdEhus/RknD3/bOd4qLYVPPuA=="], + "@towns-protocol/sdk/@towns-protocol/proto": ["@towns-protocol/proto@0.0.455", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0" } }, "sha512-jAc/w7HknqKEtBeh4Jj277ssg3TA70jv78bkBTIsyfU3WnCrTy0jl/pee181mptTbxQHylG+7mQxg077MHsPWw=="], + + "@towns-protocol/sdk-crypto/@towns-protocol/proto": ["@towns-protocol/proto@0.0.455", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0" } }, "sha512-jAc/w7HknqKEtBeh4Jj277ssg3TA70jv78bkBTIsyfU3WnCrTy0jl/pee181mptTbxQHylG+7mQxg077MHsPWw=="], + + "@towns-protocol/utils/@towns-protocol/proto": ["@towns-protocol/proto@0.0.455", "", { "dependencies": { "@bufbuild/protobuf": "^2.9.0" } }, "sha512-jAc/w7HknqKEtBeh4Jj277ssg3TA70jv78bkBTIsyfU3WnCrTy0jl/pee181mptTbxQHylG+7mQxg077MHsPWw=="], + + "@towns-protocol/web3/abitype": ["abitype@0.9.10", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-FIS7U4n7qwAT58KibwYig5iFG4K61rbhAqaQh/UWj8v1Y8mjX3F8TC9gd8cz9yT1TYel9f8nS5NO5kZp2RW0jQ=="], + + "@towns-protocol/web3/viem": ["viem@2.43.5", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.11.1", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-QuJpuEMEPM3EreN+vX4mVY68Sci0+zDxozYfbh/WfV+SSy/Gthm74PH8XmitXdty1xY54uTCJ+/Gbbd1IiMPSA=="], "@towns-protocol/web3/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1673,8 +1684,6 @@ "obj-multiplex/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "ox/abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], - "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "porto/hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], @@ -1695,8 +1704,6 @@ "valtio/use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], - "viem/abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], - "x402/viem": ["viem@2.40.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.1.0", "isows": "1.0.7", "ox": "0.9.6", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-feYfEpbgjRkZYQpwcgxqkWzjxHI5LSDAjcGetHHwDRuX9BRQHUdV8ohrCosCYpdEhus/RknD3/bOd4qLYVPPuA=="], "x402/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1925,9 +1932,9 @@ "@solana/web3.js/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], - "@towns-protocol/web3/viem/abitype": ["abitype@1.1.0", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A=="], + "@towns-protocol/web3/viem/abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], - "@towns-protocol/web3/viem/ox": ["ox@0.9.6", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg=="], + "@towns-protocol/web3/viem/ox": ["ox@0.11.1", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1l1gOLAqg0S0xiN1dH5nkPna8PucrZgrIJOfS49MLNiMevxu07Iz4ZjuJS9N+xifvT+PsZyIptS7WHM8nC+0+A=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], diff --git a/drizzle/0009_nice_eddie_brock.sql b/drizzle/0009_nice_eddie_brock.sql new file mode 100644 index 0000000..a56a1e9 --- /dev/null +++ b/drizzle/0009_nice_eddie_brock.sql @@ -0,0 +1,10 @@ +DROP INDEX "github_subscriptions_unique_idx";--> statement-breakpoint +DROP INDEX "pending_subscriptions_unique_idx";--> statement-breakpoint +ALTER TABLE "message_mappings" DROP CONSTRAINT "message_mappings_space_id_channel_id_repo_full_name_github_entity_type_github_entity_id_pk";--> statement-breakpoint +ALTER TABLE "github_subscriptions" ALTER COLUMN "space_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "message_mappings" ALTER COLUMN "space_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "oauth_states" ALTER COLUMN "space_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "pending_subscriptions" ALTER COLUMN "space_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "message_mappings" ADD CONSTRAINT "message_mappings_channel_id_repo_full_name_github_entity_type_github_entity_id_pk" PRIMARY KEY("channel_id","repo_full_name","github_entity_type","github_entity_id");--> statement-breakpoint +CREATE UNIQUE INDEX "github_subscriptions_unique_idx" ON "github_subscriptions" USING btree ("channel_id","repo_full_name");--> statement-breakpoint +CREATE UNIQUE INDEX "pending_subscriptions_unique_idx" ON "pending_subscriptions" USING btree ("channel_id","repo_full_name"); \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..c86fcae --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,911 @@ +{ + "id": "adb3fb0d-ff15-4b8b-94d3-19d271636c8d", + "prevId": "196c96a3-e7a2-4220-915b-995f48e0804d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.github_installations": { + "name": "github_installations", + "schema": "", + "columns": { + "installation_id": { + "name": "installation_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "app_slug": { + "name": "app_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'towns-github-bot'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "account_type_check": { + "name": "account_type_check", + "value": "\"github_installations\".\"account_type\" IN ('Organization', 'User')" + } + }, + "isRLSEnabled": false + }, + "public.github_subscriptions": { + "name": "github_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_mode": { + "name": "delivery_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_by_towns_user_id": { + "name": "created_by_towns_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_github_login": { + "name": "created_by_github_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installation_id": { + "name": "installation_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pr,issues,commits,releases'" + }, + "branch_filter": { + "name": "branch_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_subscriptions_unique_idx": { + "name": "github_subscriptions_unique_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_subscriptions_channel": { + "name": "idx_github_subscriptions_channel", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_subscriptions_repo": { + "name": "idx_github_subscriptions_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_subscriptions_created_by_towns_user_id_github_user_tokens_towns_user_id_fk": { + "name": "github_subscriptions_created_by_towns_user_id_github_user_tokens_towns_user_id_fk", + "tableFrom": "github_subscriptions", + "tableTo": "github_user_tokens", + "columnsFrom": [ + "created_by_towns_user_id" + ], + "columnsTo": [ + "towns_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_subscriptions_installation_id_github_installations_installation_id_fk": { + "name": "github_subscriptions_installation_id_github_installations_installation_id_fk", + "tableFrom": "github_subscriptions", + "tableTo": "github_installations", + "columnsFrom": [ + "installation_id" + ], + "columnsTo": [ + "installation_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "delivery_mode_check": { + "name": "delivery_mode_check", + "value": "\"github_subscriptions\".\"delivery_mode\" IN ('webhook', 'polling')" + } + }, + "isRLSEnabled": false + }, + "public.github_user_tokens": { + "name": "github_user_tokens", + "schema": "", + "columns": { + "towns_user_id": { + "name": "towns_user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_user_tokens_github_user_id_unique": { + "name": "github_user_tokens_github_user_id_unique", + "columns": [ + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.installation_repositories": { + "name": "installation_repositories", + "schema": "", + "columns": { + "installation_id": { + "name": "installation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_installation_repos_by_name": { + "name": "idx_installation_repos_by_name", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installation_repos_by_install": { + "name": "idx_installation_repos_by_install", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "installation_repositories_installation_id_github_installations_installation_id_fk": { + "name": "installation_repositories_installation_id_github_installations_installation_id_fk", + "tableFrom": "installation_repositories", + "tableTo": "github_installations", + "columnsFrom": [ + "installation_id" + ], + "columnsTo": [ + "installation_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "installation_repositories_installation_id_repo_full_name_pk": { + "name": "installation_repositories_installation_id_repo_full_name_pk", + "columns": [ + "installation_id", + "repo_full_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_mappings": { + "name": "message_mappings", + "schema": "", + "columns": { + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_entity_type": { + "name": "github_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_entity_id": { + "name": "github_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_type": { + "name": "parent_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_number": { + "name": "parent_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "towns_message_id": { + "name": "towns_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_updated_at": { + "name": "github_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_message_mappings_expires": { + "name": "idx_message_mappings_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "message_mappings_channel_id_repo_full_name_github_entity_type_github_entity_id_pk": { + "name": "message_mappings_channel_id_repo_full_name_github_entity_type_github_entity_id_pk", + "columns": [ + "channel_id", + "repo_full_name", + "github_entity_type", + "github_entity_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "entity_type_check": { + "name": "entity_type_check", + "value": "\"message_mappings\".\"github_entity_type\" IN ('pr', 'issue', 'comment', 'review', 'review_comment')" + }, + "parent_type_check": { + "name": "parent_type_check", + "value": "\"message_mappings\".\"parent_type\" IS NULL OR \"message_mappings\".\"parent_type\" IN ('pr', 'issue')" + } + }, + "isRLSEnabled": false + }, + "public.oauth_states": { + "name": "oauth_states", + "schema": "", + "columns": { + "state": { + "name": "state", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "towns_user_id": { + "name": "towns_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_action": { + "name": "redirect_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_data": { + "name": "redirect_data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_oauth_states_expires": { + "name": "idx_oauth_states_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_oauth_states_towns_user_id": { + "name": "idx_oauth_states_towns_user_id", + "columns": [ + { + "expression": "towns_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_subscriptions": { + "name": "pending_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "towns_user_id": { + "name": "towns_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_pending_subscriptions_expires": { + "name": "idx_pending_subscriptions_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pending_subscriptions_user": { + "name": "idx_pending_subscriptions_user", + "columns": [ + { + "expression": "towns_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pending_subscriptions_repo": { + "name": "idx_pending_subscriptions_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_subscriptions_unique_idx": { + "name": "pending_subscriptions_unique_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_subscriptions_towns_user_id_github_user_tokens_towns_user_id_fk": { + "name": "pending_subscriptions_towns_user_id_github_user_tokens_towns_user_id_fk", + "tableFrom": "pending_subscriptions", + "tableTo": "github_user_tokens", + "columnsFrom": [ + "towns_user_id" + ], + "columnsTo": [ + "towns_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repo_polling_state": { + "name": "repo_polling_state", + "schema": "", + "columns": { + "repo": { + "name": "repo", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_event_id": { + "name": "last_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_deliveries_status": { + "name": "idx_deliveries_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "status_check": { + "name": "status_check", + "value": "\"webhook_deliveries\".\"status\" IN ('pending', 'success', 'failed')" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 43aa9dd..089fba9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1765085291467, "tag": "0008_bouncy_jack_power", "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1768446708725, + "tag": "0009_nice_eddie_brock", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 347d5d0..0bfa73b 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { "name": "towns-github-bot", - "version": "1.0.1", + "version": "1.1.0", "type": "module", "main": "src/index.ts", "scripts": { "build": "tsc --noEmit", "clean": "rm -rf dist", + "db:up": "docker start github-bot-db 2>/dev/null || docker run -d --name github-bot-db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=github-bot -v github-bot-data:/var/lib/postgresql -p 5432:5432 postgres:18", + "db:down": "docker stop github-bot-db", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", @@ -22,14 +24,13 @@ "@octokit/app": "^16.1.2", "@octokit/rest": "^22.0.1", "@octokit/webhooks": "^14.2.0", - "@towns-protocol/bot": "^0.0.453", - "@towns-protocol/proto": "^0.0.453", - "@towns-protocol/web3": "^0.0.453", + "@towns-protocol/bot": "^1.0.3", + "@towns-protocol/proto": "^1.0.3", "drizzle-orm": "^0.45.1", - "hono": "^4.11.3", + "hono": "^4.11.4", "picomatch": "^4.0.3", "postgres": "^3.4.8", - "viem": "^2.43.5", + "viem": "^2.44.2", "zod": "^4.3.5" }, "devDependencies": { @@ -37,22 +38,22 @@ "@eslint/js": "^9.39.2", "@ianvs/prettier-plugin-sort-imports": "^4.7.0", "@prettier/plugin-oxc": "^0.1.3", - "@types/bun": "^1.3.5", + "@types/bun": "^1.3.6", "@types/picomatch": "^4.0.2", - "@typescript-eslint/eslint-plugin": "^8.52.0", - "@typescript-eslint/parser": "^8.52.0", + "@typescript-eslint/eslint-plugin": "^8.53.0", + "@typescript-eslint/parser": "^8.53.0", "drizzle-kit": "^0.31.8", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-formatter-unix": "^9.0.1", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import-x": "^4.16.1", - "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-tsdoc": "^0.5.0", "prettier": "^3.7.4", "prettier-plugin-ember-template-tag": "^2.1.2", "typescript": "~5.8.3", - "typescript-eslint": "^8.52.0" + "typescript-eslint": "^8.53.0" }, "files": [ "/dist" diff --git a/src/db/schema.ts b/src/db/schema.ts index 7d9b0c8..4f761c3 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -50,7 +50,7 @@ export const oauthStates = pgTable( state: text("state").primaryKey(), townsUserId: text("towns_user_id").notNull(), channelId: text("channel_id").notNull(), - spaceId: text("space_id").notNull(), + spaceId: text("space_id"), // Nullable for DM channels redirectAction: text("redirect_action"), // 'subscribe' etc redirectData: text("redirect_data"), // JSON string with additional context expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), @@ -72,7 +72,7 @@ export const githubSubscriptions = pgTable( "github_subscriptions", { id: serial("id").primaryKey(), - spaceId: text("space_id").notNull(), + spaceId: text("space_id"), // Nullable for DM channels channelId: text("channel_id").notNull(), repoFullName: text("repo_full_name").notNull(), // Format: "owner/repo" deliveryMode: text("delivery_mode").notNull(), // 'webhook' or 'polling' @@ -99,8 +99,8 @@ export const githubSubscriptions = pgTable( "delivery_mode_check", sql`${table.deliveryMode} IN ('webhook', 'polling')` ), + // channelId is globally unique in Towns, so spaceId not needed for uniqueness uniqueSubscription: uniqueIndex("github_subscriptions_unique_idx").on( - table.spaceId, table.channelId, table.repoFullName ), @@ -209,7 +209,7 @@ export const pendingSubscriptions = pgTable( .references(() => githubUserTokens.townsUserId, { onDelete: "cascade", }), - spaceId: text("space_id").notNull(), + spaceId: text("space_id"), // Nullable for DM channels channelId: text("channel_id").notNull(), repoFullName: text("repo_full_name").notNull(), eventTypes: text("event_types").notNull(), @@ -222,8 +222,8 @@ export const pendingSubscriptions = pgTable( ), userIndex: index("idx_pending_subscriptions_user").on(table.townsUserId), repoIndex: index("idx_pending_subscriptions_repo").on(table.repoFullName), + // channelId is globally unique in Towns, so spaceId not needed for uniqueness uniquePending: uniqueIndex("pending_subscriptions_unique_idx").on( - table.spaceId, table.channelId, table.repoFullName ), @@ -237,8 +237,7 @@ export const pendingSubscriptions = pgTable( export const messageMappings = pgTable( "message_mappings", { - // Composite primary key columns - spaceId: text("space_id").notNull(), + spaceId: text("space_id"), // Nullable for DM channels channelId: text("channel_id").notNull(), repoFullName: text("repo_full_name").notNull(), githubEntityType: text("github_entity_type").notNull(), // 'pr' | 'issue' | 'comment' | 'review' | 'review_comment' @@ -261,7 +260,6 @@ export const messageMappings = pgTable( table => ({ pk: primaryKey({ columns: [ - table.spaceId, table.channelId, table.repoFullName, table.githubEntityType, diff --git a/src/handlers/github-subscription-handler.ts b/src/handlers/github-subscription-handler.ts index 2a6e640..f386e2a 100644 --- a/src/handlers/github-subscription-handler.ts +++ b/src/handlers/github-subscription-handler.ts @@ -89,7 +89,7 @@ async function handleSubscribe( oauthService: GitHubOAuthService, repoArg: string | undefined ): Promise { - const { channelId, spaceId, args } = event; + const { channelId, args } = event; if (!repoArg) { await handler.sendMessage( @@ -127,7 +127,7 @@ async function handleSubscribe( // Check if already subscribed - if so, add event types instead (case-insensitive match) const channelSubscriptions = - await subscriptionService.getChannelSubscriptions(channelId, spaceId); + await subscriptionService.getChannelSubscriptions(channelId); const existingSubscription = channelSubscriptions.find( sub => sub.repo.toLowerCase() === repo.toLowerCase() ); @@ -274,7 +274,6 @@ async function handleUpdateSubscription( // Update subscription (add event types and/or update branch filter) const updateResult = await subscriptionService.updateSubscription( userId, - spaceId, channelId, repo, eventTypes, @@ -306,7 +305,7 @@ async function handleUnsubscribe( oauthService: GitHubOAuthService, repoArg: string | undefined ): Promise { - const { channelId, spaceId, args } = event; + const { channelId, args } = event; if (!repoArg) { await handler.sendMessage( @@ -326,10 +325,8 @@ async function handleUnsubscribe( } // Check if channel has any subscriptions - const channelRepos = await subscriptionService.getChannelSubscriptions( - channelId, - spaceId - ); + const channelRepos = + await subscriptionService.getChannelSubscriptions(channelId); if (channelRepos.length === 0) { await handler.sendMessage( channelId, @@ -451,7 +448,6 @@ async function handleRemoveEventTypes( // Remove event types (validates repo access) const removeResult = await subscriptionService.removeEventTypes( userId, - spaceId, channelId, repo, typesToRemove as EventType[] @@ -517,7 +513,6 @@ async function handleFullUnsubscribe( // Use removeEventTypes with all event types - validates repo access and deletes subscription const removeResult = await subscriptionService.removeEventTypes( userId, - spaceId, channelId, repo, eventTypes @@ -539,12 +534,10 @@ async function handleStatus( event: SlashCommandEvent, subscriptionService: SubscriptionService ): Promise { - const { channelId, spaceId } = event; + const { channelId } = event; - const subscriptions = await subscriptionService.getChannelSubscriptions( - channelId, - spaceId - ); + const subscriptions = + await subscriptionService.getChannelSubscriptions(channelId); if (subscriptions.length === 0) { await handler.sendMessage( channelId, diff --git a/src/routes/oauth-callback.ts b/src/routes/oauth-callback.ts index dcdcc73..06d9cb6 100644 --- a/src/routes/oauth-callback.ts +++ b/src/routes/oauth-callback.ts @@ -62,7 +62,7 @@ export async function handleOAuthCallback( // If there was a redirect action (e.g., subscribe), complete the subscription if (redirectAction === "subscribe" && redirectData) { - if (redirectData.repo && spaceId && townsUserId) { + if (redirectData.repo && townsUserId) { const eventTypes: EventType[] = redirectData.eventTypes ?? [ ...DEFAULT_EVENT_TYPES_ARRAY, ]; @@ -71,7 +71,7 @@ export async function handleOAuthCallback( const branchFilter = redirectData.branchFilter ?? null; const subResult = await subscriptionService.createSubscription({ townsUserId, - spaceId, + spaceId, // May be null for DMs channelId, repoIdentifier: redirectData.repo, eventTypes, @@ -112,14 +112,13 @@ export async function handleOAuthCallback( // Handle subscription update (add event types to existing subscription) if (redirectAction === "subscribe-update" && redirectData) { - if (redirectData.repo && spaceId && townsUserId) { + if (redirectData.repo && townsUserId) { const eventTypes: EventType[] = redirectData.eventTypes ?? [ ...DEFAULT_EVENT_TYPES_ARRAY, ]; const updateResult = await subscriptionService.updateSubscription( townsUserId, - spaceId, channelId, redirectData.repo, eventTypes, @@ -147,15 +146,9 @@ export async function handleOAuthCallback( // Handle unsubscribe update (remove event types from existing subscription) if (redirectAction === "unsubscribe-update" && redirectData) { - if ( - redirectData.repo && - spaceId && - townsUserId && - redirectData.eventTypes - ) { + if (redirectData.repo && townsUserId && redirectData.eventTypes) { const removeResult = await subscriptionService.removeEventTypes( townsUserId, - spaceId, channelId, redirectData.repo, redirectData.eventTypes diff --git a/src/services/github-oauth-service.ts b/src/services/github-oauth-service.ts index 954d0d0..3cdddd0 100644 --- a/src/services/github-oauth-service.ts +++ b/src/services/github-oauth-service.ts @@ -28,7 +28,7 @@ import { export interface OAuthCallbackResult { townsUserId: string; channelId: string; - spaceId: string; + spaceId: string | undefined; // Undefined for DM channels redirectAction: RedirectAction | null; redirectData: RedirectData | null; githubLogin: string; @@ -102,7 +102,7 @@ export class GitHubOAuthService { * * @param townsUserId - Towns user ID * @param channelId - Current channel ID - * @param spaceId - Current space ID + * @param spaceId - Current space ID (undefined for DM channels) * @param redirectAction - Action to perform after OAuth (e.g., 'subscribe') * @param redirectData - Additional data for redirect (e.g., repo name) * @returns Authorization URL to send to user @@ -110,7 +110,7 @@ export class GitHubOAuthService { async getAuthorizationUrl( townsUserId: string, channelId: string, - spaceId: string, + spaceId: string | undefined, redirectAction?: RedirectAction, redirectData?: RedirectData ): Promise { @@ -123,7 +123,7 @@ export class GitHubOAuthService { state, townsUserId, channelId, - spaceId, + spaceId: spaceId ?? null, // Convert undefined to null for DB redirectAction: redirectAction || null, redirectData: redirectData ? JSON.stringify(redirectData) : null, expiresAt, @@ -237,7 +237,7 @@ export class GitHubOAuthService { return { townsUserId: stateData.townsUserId, channelId: stateData.channelId, - spaceId: stateData.spaceId, + spaceId: stateData.spaceId ?? undefined, // Convert null to undefined redirectAction: actionResult.success ? actionResult.data : null, redirectData: dataResult?.success ? dataResult.data : null, githubLogin: user.login, diff --git a/src/services/message-delivery-service.ts b/src/services/message-delivery-service.ts index 46af3d7..74b0707 100644 --- a/src/services/message-delivery-service.ts +++ b/src/services/message-delivery-service.ts @@ -35,7 +35,7 @@ export interface EntityContext { * Parameters for delivering a message */ export interface DeliveryParams { - spaceId: string; + spaceId: string | null; // Null for DM channels channelId: string; repoFullName: string; action: DeliveryAction; @@ -84,7 +84,6 @@ export class MessageDeliveryService { if (delay) await new Promise(r => setTimeout(r, delay)); threadId = (await this.getMessageId( - spaceId, channelId, repoFullName, entityContext.parentType, @@ -125,12 +124,7 @@ export class MessageDeliveryService { console.log("Delete action requires entityContext"); return; } - await this.handleDelete( - spaceId, - channelId, - repoFullName, - entityContext - ); + await this.handleDelete(channelId, repoFullName, entityContext); return; case "edit": { @@ -176,14 +170,12 @@ export class MessageDeliveryService { } private async handleDelete( - spaceId: string, channelId: string, repoFullName: string, entityContext: EntityContext ): Promise { const { githubEntityType, githubEntityId } = entityContext; const existingMessageId = await this.getMessageId( - spaceId, channelId, repoFullName, githubEntityType, @@ -193,7 +185,6 @@ export class MessageDeliveryService { if (existingMessageId) { await this.bot.removeEvent(channelId, existingMessageId); await this.deleteMapping( - spaceId, channelId, repoFullName, githubEntityType, @@ -206,7 +197,7 @@ export class MessageDeliveryService { } private async handleEdit( - spaceId: string, + spaceId: string | null, channelId: string, repoFullName: string, entityContext: EntityContext, @@ -214,7 +205,6 @@ export class MessageDeliveryService { ): Promise { const { githubEntityType, githubEntityId, githubUpdatedAt } = entityContext; const existingMessageId = await this.getMessageId( - spaceId, channelId, repoFullName, githubEntityType, @@ -247,7 +237,6 @@ export class MessageDeliveryService { // Check if we should process this update if (githubUpdatedAt) { const shouldUpdate = await this.shouldUpdate( - spaceId, channelId, repoFullName, githubEntityType, @@ -283,7 +272,7 @@ export class MessageDeliveryService { } private async handleCreate( - spaceId: string, + spaceId: string | null, channelId: string, repoFullName: string, entityContext: EntityContext | undefined, @@ -319,7 +308,6 @@ export class MessageDeliveryService { * Uses gt(expiresAt, now) so expired rows are ignored even before cleanup runs. */ private async getMessageId( - spaceId: string, channelId: string, repoFullName: string, entityType: GithubEntityType, @@ -330,7 +318,6 @@ export class MessageDeliveryService { .from(messageMappings) .where( and( - eq(messageMappings.spaceId, spaceId), eq(messageMappings.channelId, channelId), eq(messageMappings.repoFullName, repoFullName), eq(messageMappings.githubEntityType, entityType), @@ -345,7 +332,7 @@ export class MessageDeliveryService { private async storeMapping( params: { - spaceId: string; + spaceId: string | null; channelId: string; repoFullName: string; githubEntityType: GithubEntityType; @@ -370,13 +357,13 @@ export class MessageDeliveryService { }) .onConflictDoUpdate({ target: [ - messageMappings.spaceId, messageMappings.channelId, messageMappings.repoFullName, messageMappings.githubEntityType, messageMappings.githubEntityId, ], set: { + spaceId: params.spaceId, townsMessageId: params.townsMessageId, githubUpdatedAt: params.githubUpdatedAt, expiresAt, @@ -385,7 +372,6 @@ export class MessageDeliveryService { } private async shouldUpdate( - spaceId: string, channelId: string, repoFullName: string, entityType: GithubEntityType, @@ -397,7 +383,6 @@ export class MessageDeliveryService { .from(messageMappings) .where( and( - eq(messageMappings.spaceId, spaceId), eq(messageMappings.channelId, channelId), eq(messageMappings.repoFullName, repoFullName), eq(messageMappings.githubEntityType, entityType), @@ -413,7 +398,6 @@ export class MessageDeliveryService { } private async deleteMapping( - spaceId: string, channelId: string, repoFullName: string, entityType: GithubEntityType, @@ -423,7 +407,6 @@ export class MessageDeliveryService { .delete(messageMappings) .where( and( - eq(messageMappings.spaceId, spaceId), eq(messageMappings.channelId, channelId), eq(messageMappings.repoFullName, repoFullName), eq(messageMappings.githubEntityType, entityType), diff --git a/src/services/subscription-service.ts b/src/services/subscription-service.ts index 5924562..d404aca 100644 --- a/src/services/subscription-service.ts +++ b/src/services/subscription-service.ts @@ -43,7 +43,7 @@ export type BranchFilter = string | null; */ export interface SubscribeParams { townsUserId: string; - spaceId: string; + spaceId?: string; // Undefined for DM channels channelId: string; repoIdentifier: string; // Format: "owner/repo" eventTypes: EventType[]; @@ -226,13 +226,12 @@ export class SubscriptionService { deliveryMode = installationId ? "webhook" : "polling"; } - // 4. Check if already subscribed + // 4. Check if already subscribed (channelId is globally unique) const existing = await db .select() .from(githubSubscriptions) .where( and( - eq(githubSubscriptions.spaceId, spaceId), eq(githubSubscriptions.channelId, channelId), eq(githubSubscriptions.repoFullName, repoInfo.fullName) ) @@ -250,7 +249,7 @@ export class SubscriptionService { // 5. Create subscription const now = new Date(); await db.insert(githubSubscriptions).values({ - spaceId, + spaceId: spaceId ?? null, // Store spaceId when available, null for DMs channelId, repoFullName: repoInfo.fullName, deliveryMode, @@ -290,7 +289,6 @@ export class SubscriptionService { */ async updateSubscription( townsUserId: string, - spaceId: string, channelId: string, repoFullName: string, newEventTypes: EventType[], @@ -303,7 +301,6 @@ export class SubscriptionService { }> { const validation = await this.validateRepoAccessAndGetSubscription( townsUserId, - spaceId, channelId, repoFullName ); @@ -321,7 +318,7 @@ export class SubscriptionService { const finalBranchFilter = branchFilter !== undefined ? branchFilter : currentBranchFilter; - // Update subscription + // Update subscription (channelId is globally unique) const result = await db .update(githubSubscriptions) .set({ @@ -331,7 +328,6 @@ export class SubscriptionService { }) .where( and( - eq(githubSubscriptions.spaceId, spaceId), eq(githubSubscriptions.channelId, channelId), eq(githubSubscriptions.repoFullName, repoFullName) ) @@ -360,7 +356,6 @@ export class SubscriptionService { */ async removeEventTypes( townsUserId: string, - spaceId: string, channelId: string, repoFullName: string, typesToRemove: EventType[] @@ -372,7 +367,6 @@ export class SubscriptionService { }> { const validation = await this.validateRepoAccessAndGetSubscription( townsUserId, - spaceId, channelId, repoFullName ); @@ -388,7 +382,7 @@ export class SubscriptionService { // If no types remain, delete the subscription if (remainingTypes.length === 0) { - const deleted = await this.unsubscribe(channelId, spaceId, repoFullName); + const deleted = await this.unsubscribe(channelId, repoFullName); if (!deleted) { return { success: false, @@ -401,7 +395,7 @@ export class SubscriptionService { }; } - // Update subscription with remaining types + // Update subscription with remaining types (channelId is globally unique) const updated = await db .update(githubSubscriptions) .set({ @@ -410,7 +404,6 @@ export class SubscriptionService { }) .where( and( - eq(githubSubscriptions.spaceId, spaceId), eq(githubSubscriptions.channelId, channelId), eq(githubSubscriptions.repoFullName, repoFullName) ) @@ -468,7 +461,7 @@ export class SubscriptionService { // Create the subscription (will be webhook mode since installation exists) const result = await this.createSubscription({ townsUserId: sub.townsUserId, - spaceId: sub.spaceId, + spaceId: sub.spaceId ?? undefined, channelId: sub.channelId, repoIdentifier: repoFullName, eventTypes, @@ -507,16 +500,12 @@ export class SubscriptionService { /** * Unsubscribe a channel from a repository */ - async unsubscribe( - channelId: string, - spaceId: string, - repoFullName: string - ): Promise { + async unsubscribe(channelId: string, repoFullName: string): Promise { + // channelId is globally unique in Towns const result = await db .delete(githubSubscriptions) .where( and( - eq(githubSubscriptions.spaceId, spaceId), eq(githubSubscriptions.channelId, channelId), eq(githubSubscriptions.repoFullName, repoFullName) ) @@ -530,7 +519,6 @@ export class SubscriptionService { * Get a specific subscription */ async getSubscription( - spaceId: string, channelId: string, repoFullName: string ): Promise<{ @@ -540,6 +528,7 @@ export class SubscriptionService { createdByTownsUserId: string; branchFilter: BranchFilter; } | null> { + // channelId is globally unique in Towns const results = await db .select({ id: githubSubscriptions.id, @@ -551,7 +540,6 @@ export class SubscriptionService { .from(githubSubscriptions) .where( and( - eq(githubSubscriptions.spaceId, spaceId), eq(githubSubscriptions.channelId, channelId), eq(githubSubscriptions.repoFullName, repoFullName) ) @@ -569,10 +557,7 @@ export class SubscriptionService { /** * Get all subscriptions for a channel */ - async getChannelSubscriptions( - channelId: string, - spaceId: string - ): Promise< + async getChannelSubscriptions(channelId: string): Promise< Array<{ repo: string; eventTypes: EventType[]; @@ -580,6 +565,7 @@ export class SubscriptionService { branchFilter: BranchFilter; }> > { + // channelId is globally unique in Towns const results = await db .select({ repo: githubSubscriptions.repoFullName, @@ -588,12 +574,7 @@ export class SubscriptionService { branchFilter: githubSubscriptions.branchFilter, }) .from(githubSubscriptions) - .where( - and( - eq(githubSubscriptions.channelId, channelId), - eq(githubSubscriptions.spaceId, spaceId) - ) - ); + .where(eq(githubSubscriptions.channelId, channelId)); return results.map(r => ({ repo: r.repo, @@ -616,7 +597,7 @@ export class SubscriptionService { deliveryMode?: "webhook" | "polling" ): Promise< Array<{ - spaceId: string; + spaceId: string | null; channelId: string; eventTypes: EventType[]; branchFilter: BranchFilter; @@ -948,7 +929,7 @@ export class SubscriptionService { */ private async requiresInstallationFailure(params: { townsUserId: string; - spaceId: string; + spaceId?: string; channelId: string; repoFullName: string; eventTypes: EventType[]; @@ -978,7 +959,7 @@ export class SubscriptionService { */ private async storePendingSubscription(params: { townsUserId: string; - spaceId: string; + spaceId?: string; channelId: string; repoFullName: string; eventTypes: EventType[]; @@ -992,7 +973,7 @@ export class SubscriptionService { .insert(pendingSubscriptions) .values({ townsUserId: params.townsUserId, - spaceId: params.spaceId, + spaceId: params.spaceId ?? null, channelId: params.channelId, repoFullName: params.repoFullName, eventTypes: params.eventTypes.join(","), @@ -1013,7 +994,6 @@ export class SubscriptionService { */ private async validateRepoAccessAndGetSubscription( townsUserId: string, - spaceId: string, channelId: string, repoFullName: string ): Promise< @@ -1037,11 +1017,7 @@ export class SubscriptionService { }; } - const subscription = await this.getSubscription( - spaceId, - channelId, - repoFullName - ); + const subscription = await this.getSubscription(channelId, repoFullName); if (!subscription) { return { success: false, error: `Not subscribed to ${repoFullName}` }; } diff --git a/src/utils/oauth-helpers.ts b/src/utils/oauth-helpers.ts index 88e12ec..8ec4751 100644 --- a/src/utils/oauth-helpers.ts +++ b/src/utils/oauth-helpers.ts @@ -20,7 +20,7 @@ export async function sendQueryOAuthPrompt( handler: BotHandler, userId: string, channelId: string, - spaceId: string, + spaceId: string | undefined, repo: string ): Promise { return sendEditableOAuthPrompt( @@ -61,7 +61,7 @@ export async function sendEditableOAuthPrompt( handler: BotHandler, userId: string, channelId: string, - spaceId: string, + spaceId: string | undefined, message: string, redirectAction: RedirectAction, redirectData: Omit @@ -119,7 +119,7 @@ export async function handleInvalidOAuthToken( handler: BotHandler, userId: string, channelId: string, - spaceId: string, + spaceId: string | undefined, redirectAction: RedirectAction, redirectData: { repo: string; diff --git a/tests/unit/handlers/github-subscription-handler.test.ts b/tests/unit/handlers/github-subscription-handler.test.ts index d172dcf..5eb2683 100644 --- a/tests/unit/handlers/github-subscription-handler.test.ts +++ b/tests/unit/handlers/github-subscription-handler.test.ts @@ -519,7 +519,6 @@ describe("github subscription handler", () => { expect(removeCalls.length).toBe(1); expect(removeCalls[0]).toEqual([ "0x123", - "test-space", "test-channel", "owner/repo", ["pr", "issues"], @@ -551,7 +550,7 @@ describe("github subscription handler", () => { ); const removeCalls = mockSubscriptionService.removeEventTypes.mock.calls; - expect(removeCalls[0][3]).toBe("owner/repo"); + expect(removeCalls[0][2]).toBe("owner/repo"); }); test("should handle unsubscribe failure", async () => { @@ -602,7 +601,7 @@ describe("github subscription handler", () => { const removeCalls = mockSubscriptionService.removeEventTypes.mock.calls; expect(removeCalls.length).toBe(1); - expect(removeCalls[0][3]).toBe("owner/repo"); + expect(removeCalls[0][2]).toBe("owner/repo"); }); }); @@ -786,7 +785,7 @@ describe("github subscription handler", () => { const updateCalls = mockSubscriptionService.updateSubscription.mock.calls; expect(updateCalls.length).toBe(1); - expect(updateCalls[0][5]).toBe("main,develop"); + expect(updateCalls[0][4]).toBe("main,develop"); }); }); });