From 29e29364e71896a4b58d3b02fa2aeab3488881ee Mon Sep 17 00:00:00 2001 From: Mat Manna Date: Tue, 4 Aug 2026 09:25:29 -0400 Subject: [PATCH] feat: docs and api and keys --- .gitignore | 1 + bun.lock | 39 + docs.html | 5 +- index.html | 4 +- keys.html | 19 + package-lock.json | 2019 ++++++++++++++++++++++++++++-- package.json | 3 + src/api/client.ts | 10 + src/api/context.ts | 60 + src/api/router.ts | 103 +- src/api/routers/apiKeys.ts | 84 ++ src/api/routers/channels.ts | 70 ++ src/api/routers/messages.ts | 58 + src/api/routers/subscriptions.ts | 52 + src/app.ts | 542 ++++++-- src/db/index.ts | 10 + src/db/migrate.ts | 95 ++ src/db/schema.ts | 105 +- src/env.d.ts | 5 + src/lib/api-keys.ts | 123 ++ src/lib/auth-client.ts | 7 + src/lib/auth.ts | 102 ++ src/store/neon.ts | 53 +- src/store/pg.test.ts | 14 +- src/store/pg.ts | 54 +- src/store/store.ts | 2 + src/web/app.tsx | 11 +- src/web/docs.tsx | 11 +- src/web/keys.tsx | 118 ++ src/web/lockdown.ts | 8 +- src/web/navbar.tsx | 65 +- src/web/style.css | 92 +- src/web/usage.tsx | 14 +- usage.html | 5 +- vite.config.ts | 3 +- wrangler.jsonc | 75 +- 36 files changed, 3715 insertions(+), 326 deletions(-) create mode 100644 keys.html create mode 100644 src/api/client.ts create mode 100644 src/api/context.ts create mode 100644 src/api/routers/apiKeys.ts create mode 100644 src/api/routers/channels.ts create mode 100644 src/api/routers/messages.ts create mode 100644 src/api/routers/subscriptions.ts create mode 100644 src/db/index.ts create mode 100644 src/lib/api-keys.ts create mode 100644 src/lib/auth-client.ts create mode 100644 src/lib/auth.ts create mode 100644 src/web/keys.tsx diff --git a/.gitignore b/.gitignore index 30a894d..b9dd8a2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store +.wrangler diff --git a/bun.lock b/bun.lock index 1373986..f9c7ed8 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "@slack/web-api": "^7.18.0", "@vitejs/plugin-react": "^6.0.4", "@xyflow/react": "^12.11.2", + "better-auth": "^1.6.25", "drizzle-orm": "^0.45.2", "feed": "^5.2.1", "hono": "^4.12.30", @@ -82,6 +83,24 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@better-auth/core": ["@better-auth/core@1.6.25", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.7", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-lMTlhtwyK4NpY9kPF+2rQCRKYpg136d3gM2xl8esxT1PjJx5Nh5YwZvxcYCIjDuO759sx6TCloJTuwcZGG6ZBw=="], + + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-ru/DeKjFPQUVeKkxF/ScazmPqIY7lwfkAV5Yt4j24wmn1Y8vFwoiPRnHgXUeZqBs10+nubaRwEqLF39CP6EhRw=="], + + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-zxiePhtN1YClS1irKYPVwWfN6kYp+QoYlz1hdQUOj8hXyo2aE/ny4RNAb6v332b0+U6Vu88EhYITRPdmvCo6uA=="], + + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2" } }, "sha512-GhEzTumc8yfTz+OZ6pMg06BA49xob49x1bX+1mEl/FStDJoSF+6mTfI5M2ytFxaiN89336/aUjkW8u+qRyLexw=="], + + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-ZtMmjcOdXR2Ziqx5y8ptTOaNpe0snNfALbBUPXJsgeyeRkDJDYzyLZ8MpuvNBTNllNeIFDbiXWAK5k+pEBZrUQ=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-ym7B6Iqcry+/4aQnYpFwqP/GBIiXvjrm/5B6+0qmx8mkTY/apHFTpHuGzUYYNf4vPTtzF3eYY2+s2GOsomKaRg=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-2ZfC9lp7tU6Jw/q2Lz/bKfQqGMdMwc/IQDTYdBhvtGi24qInYVnhp2ZCW57hHM9j+fq1ULOtxgg6M3T1LEaihw=="], + + "@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], @@ -234,6 +253,12 @@ "@neondatabase/serverless": ["@neondatabase/serverless@0.10.4", "", { "dependencies": { "@types/pg": "8.11.6" } }, "sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA=="], + "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + "@orpc/client": ["@orpc/client@1.14.7", "", { "dependencies": { "@orpc/shared": "1.14.7", "@orpc/standard-server": "1.14.7", "@orpc/standard-server-fetch": "1.14.7", "@orpc/standard-server-peer": "1.14.7" } }, "sha512-/yloM96/6Z3qlmEgW4/VMYUnhxXbCr6lIJiKSHmsvhgKcwrUPOPJrL3p0ZpItz5SKVtQSwXIhT5KcT/3va/Aew=="], "@orpc/contract": ["@orpc/contract@1.14.7", "", { "dependencies": { "@orpc/client": "1.14.7", "@orpc/shared": "1.14.7", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-LVAzjOfwW+5L4z5hGOkGGsocFF4URLmQBujrvUr6/TLeDmiiY2kB89jyJ793U40ol9/Ba935g/b0pBajiQhPWA=="], @@ -424,6 +449,10 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.11.6", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-69D/imtToCsIcAl8WBS2YaRwA4jO/j0HhU+hELqMEu9f54MoUtI6+XH5mrKU8rEFNEk/Ui1I2MK4/JkWacclGw=="], + "better-auth": ["better-auth@1.6.25", "", { "dependencies": { "@better-auth/core": "1.6.25", "@better-auth/drizzle-adapter": "1.6.25", "@better-auth/kysely-adapter": "1.6.25", "@better-auth/memory-adapter": "1.6.25", "@better-auth/mongo-adapter": "1.6.25", "@better-auth/prisma-adapter": "1.6.25", "@better-auth/telemetry": "1.6.25", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.7", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-fvoq+oCO+FF5fpP3XfU7znRyGFpHB77UG2EyxsKNy+Cak7Q5pELu+auvvDveQbWQxcoKugZ7jYQQPFQLpUTGOw=="], + + "better-call": ["better-call@1.3.7", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="], @@ -508,6 +537,8 @@ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -650,6 +681,8 @@ "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "jose": ["jose@6.2.7", "", {}, "sha512-hq1OB1bALKfydZNoViyg6hPVGV4i93ny9Op+n4zP5RSf7SCZEXa/TsG2O3IEr7+WlHRTPnpqDmHfMH6qXAD60w=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -662,6 +695,8 @@ "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], + "kysely": ["kysely@0.29.4", "", {}, "sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], @@ -804,6 +839,8 @@ "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "nanostores": ["nanostores@1.4.2", "", {}, "sha512-Wxv8Roefr2nqtiRG0bnaFlpYqpIVtOEeJZHaH+4nGgOK1/7n6OHOuHCb/bhqrNQgZM8fyd0s1PqhdrJc9Ib44g=="], + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "node-html-parser": ["node-html-parser@6.1.13", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg=="], @@ -928,6 +965,8 @@ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="], diff --git a/docs.html b/docs.html index 7710155..a8e9d32 100644 --- a/docs.html +++ b/docs.html @@ -2,8 +2,11 @@ + + + - indigest — API Docs + indigest — api docs diff --git a/index.html b/index.html index a8af016..7df0f07 100644 --- a/index.html +++ b/index.html @@ -2,8 +2,10 @@ + + - indigest — Channel Feed Map + indigest - feed map diff --git a/keys.html b/keys.html new file mode 100644 index 0000000..2d041f5 --- /dev/null +++ b/keys.html @@ -0,0 +1,19 @@ + + + + + + + + + indigest - your api keys + + + + +
+ + + + diff --git a/package-lock.json b/package-lock.json index d8ff1d2..b977c74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,13 @@ "@orpc/client": "^1.14.7", "@orpc/openapi": "^1.14.7", "@orpc/server": "^1.14.7", + "@orpc/zod": "^1.14.7", "@preact/signals": "^2.10.0", "@slack/events-api": "^3.0.1", "@slack/web-api": "^7.18.0", "@vitejs/plugin-react": "^6.0.4", "@xyflow/react": "^12.11.2", + "better-auth": "^1.6.25", "drizzle-orm": "^0.45.2", "feed": "^5.2.1", "hono": "^4.12.30", @@ -22,6 +24,8 @@ "preact": "^10.29.7", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", "zod": "^4.4.3" }, "devDependencies": { @@ -30,6 +34,7 @@ "@preact/preset-vite": "^2.10.6", "@types/bun": "latest", "drizzle-kit": "^0.31.10", + "typescript": "^5.9.3", "vite": "^8.1.5", "wrangler": "^4.113.0" }, @@ -319,6 +324,139 @@ "node": ">=6.9.0" } }, + "node_modules/@better-auth/core": { + "version": "1.6.25", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.25.tgz", + "integrity": "sha512-lMTlhtwyK4NpY9kPF+2rQCRKYpg136d3gM2xl8esxT1PjJx5Nh5YwZvxcYCIjDuO759sx6TCloJTuwcZGG6ZBw==", + "license": "MIT", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.39.0", + "@standard-schema/spec": "^1.1.0", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1", + "@cloudflare/workers-types": ">=4", + "@opentelemetry/api": "^1.9.0", + "better-call": "1.3.7", + "jose": "^6.1.0", + "kysely": "^0.28.5 || ^0.29.0", + "nanostores": "^1.0.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@better-auth/drizzle-adapter": { + "version": "1.6.25", + "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.25.tgz", + "integrity": "sha512-ru/DeKjFPQUVeKkxF/ScazmPqIY7lwfkAV5Yt4j24wmn1Y8vFwoiPRnHgXUeZqBs10+nubaRwEqLF39CP6EhRw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.25", + "@better-auth/utils": "0.4.2", + "drizzle-orm": "^0.45.2" + }, + "peerDependenciesMeta": { + "drizzle-orm": { + "optional": true + } + } + }, + "node_modules/@better-auth/kysely-adapter": { + "version": "1.6.25", + "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.25.tgz", + "integrity": "sha512-zxiePhtN1YClS1irKYPVwWfN6kYp+QoYlz1hdQUOj8hXyo2aE/ny4RNAb6v332b0+U6Vu88EhYITRPdmvCo6uA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.25", + "@better-auth/utils": "0.4.2", + "kysely": "^0.28.17 || ^0.29.0" + }, + "peerDependenciesMeta": { + "kysely": { + "optional": true + } + } + }, + "node_modules/@better-auth/memory-adapter": { + "version": "1.6.25", + "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.25.tgz", + "integrity": "sha512-GhEzTumc8yfTz+OZ6pMg06BA49xob49x1bX+1mEl/FStDJoSF+6mTfI5M2ytFxaiN89336/aUjkW8u+qRyLexw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.25", + "@better-auth/utils": "0.4.2" + } + }, + "node_modules/@better-auth/mongo-adapter": { + "version": "1.6.25", + "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.25.tgz", + "integrity": "sha512-ZtMmjcOdXR2Ziqx5y8ptTOaNpe0snNfALbBUPXJsgeyeRkDJDYzyLZ8MpuvNBTNllNeIFDbiXWAK5k+pEBZrUQ==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.25", + "@better-auth/utils": "0.4.2", + "mongodb": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "mongodb": { + "optional": true + } + } + }, + "node_modules/@better-auth/prisma-adapter": { + "version": "1.6.25", + "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.25.tgz", + "integrity": "sha512-ym7B6Iqcry+/4aQnYpFwqP/GBIiXvjrm/5B6+0qmx8mkTY/apHFTpHuGzUYYNf4vPTtzF3eYY2+s2GOsomKaRg==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.25", + "@better-auth/utils": "0.4.2", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "node_modules/@better-auth/telemetry": { + "version": "1.6.25", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.25.tgz", + "integrity": "sha512-2ZfC9lp7tU6Jw/q2Lz/bKfQqGMdMwc/IQDTYdBhvtGi24qInYVnhp2ZCW57hHM9j+fq1ULOtxgg6M3T1LEaihw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.25", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1" + } + }, + "node_modules/@better-auth/utils": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz", + "integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.0.1" + } + }, + "node_modules/@better-fetch/fetch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.1.tgz", + "integrity": "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==", + "license": "MIT" + }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.5.0", "dev": true, @@ -482,7 +620,7 @@ "version": "5.20260729.1", "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260729.1.tgz", "integrity": "sha512-X5r/4y0gKMq/B72qkz/tEwNK4c3v2regT3to6Ia8qqC66E0+YIN/fU7x0JG6ej2rLxtU3TV3aVhfK9y8+jMMAw==", - "devOptional": true, + "dev": true, "license": "MIT OR Apache-2.0" }, "node_modules/@cspotcode/source-map-support": { @@ -501,40 +639,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/@emnapi/core": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", - "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "2.0.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", - "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", - "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "dev": true, @@ -968,6 +1072,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -984,6 +1089,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1000,6 +1106,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1016,6 +1123,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1032,6 +1140,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1048,6 +1157,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1064,6 +1174,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1080,6 +1191,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1096,6 +1208,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1112,6 +1225,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1128,6 +1242,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1144,6 +1259,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1160,6 +1276,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1176,6 +1293,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1192,6 +1310,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1208,6 +1327,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1224,6 +1344,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1240,6 +1361,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1256,6 +1378,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1272,6 +1395,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1288,6 +1412,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1304,6 +1429,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1320,6 +1446,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1336,6 +1463,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1352,6 +1480,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1368,6 +1497,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2015,6 +2145,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2039,6 +2170,39 @@ "@types/pg": "8.11.6" } }, + "node_modules/@noble/ciphers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz", + "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@orpc/client": { "version": "1.14.7", "license": "MIT", @@ -2063,6 +2227,20 @@ "version": "1.14.7", "license": "MIT" }, + "node_modules/@orpc/json-schema": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/@orpc/json-schema/-/json-schema-1.14.7.tgz", + "integrity": "sha512-N4yvqv6f3BXLh5VABXiwUqhrEJFQLE/c8arIwa0tGRhXqAFXFQqaeGHYD+POGVQT7muu7l4yvnJd8jBJOkcsfQ==", + "license": "MIT", + "dependencies": { + "@orpc/contract": "1.14.7", + "@orpc/interop": "1.14.7", + "@orpc/openapi": "1.14.7", + "@orpc/server": "1.14.7", + "@orpc/shared": "1.14.7", + "json-schema-typed": "^8.0.2" + } + }, "node_modules/@orpc/openapi": { "version": "1.14.7", "license": "MIT", @@ -2192,8 +2370,27 @@ "@orpc/standard-server": "1.14.7" } }, + "node_modules/@orpc/zod": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/@orpc/zod/-/zod-1.14.7.tgz", + "integrity": "sha512-t4JCHF70QrV9apYKuJE4dUl0AwoHcjenPbSPfP7j76u50qDlean3E5XGQkxTS4FHWbwlOVY5bhSQzKcegN/MBg==", + "license": "MIT", + "dependencies": { + "@orpc/json-schema": "1.14.7", + "@orpc/openapi": "1.14.7", + "@orpc/shared": "1.14.7", + "escape-string-regexp": "^5.0.0", + "wildcard-match": "^5.1.4" + }, + "peerDependencies": { + "@orpc/contract": "1.14.7", + "@orpc/server": "1.14.7", + "zod": ">=3.25.0" + } + }, "node_modules/@oxc-project/types": { "version": "0.139.0", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -2329,6 +2526,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2345,6 +2543,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2361,6 +2560,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2377,6 +2577,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2393,6 +2594,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2409,6 +2611,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -2428,6 +2631,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -2447,6 +2651,7 @@ "cpu": [ "ppc64" ], + "dev": true, "libc": [ "glibc" ], @@ -2466,6 +2671,7 @@ "cpu": [ "s390x" ], + "dev": true, "libc": [ "glibc" ], @@ -2483,6 +2689,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -2500,6 +2707,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -2519,6 +2727,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2535,6 +2744,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2550,6 +2760,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2561,6 +2772,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2571,6 +2783,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2584,6 +2797,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2600,6 +2814,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2742,6 +2957,7 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2831,9 +3047,17 @@ }, "node_modules/@types/estree": { "version": "1.0.9", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/express": { "version": "4.17.25", "license": "MIT", @@ -2854,6 +3078,15 @@ "@types/send": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.5", "license": "MIT" @@ -2869,6 +3102,15 @@ "@types/lodash": "*" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "license": "MIT" @@ -2929,6 +3171,12 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "15.0.20", "license": "MIT", @@ -2940,6 +3188,12 @@ "version": "21.0.3", "license": "MIT" }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", @@ -3095,6 +3349,16 @@ "@babel/core": "^7.12.10" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.6", "dev": true, @@ -3106,18 +3370,143 @@ "node": ">=6.0.0" } }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "dev": true, - "license": "MIT" + "node_modules/better-auth": { + "version": "1.6.25", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.25.tgz", + "integrity": "sha512-fvoq+oCO+FF5fpP3XfU7znRyGFpHB77UG2EyxsKNy+Cak7Q5pELu+auvvDveQbWQxcoKugZ7jYQQPFQLpUTGOw==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.6.25", + "@better-auth/drizzle-adapter": "1.6.25", + "@better-auth/kysely-adapter": "1.6.25", + "@better-auth/memory-adapter": "1.6.25", + "@better-auth/mongo-adapter": "1.6.25", + "@better-auth/prisma-adapter": "1.6.25", + "@better-auth/telemetry": "1.6.25", + "@better-auth/utils": "0.4.2", + "@better-fetch/fetch": "1.3.1", + "@noble/ciphers": "^2.1.1", + "@noble/hashes": "^2.0.1", + "better-call": "1.3.7", + "defu": "^6.1.4", + "jose": "^6.1.3", + "kysely": "^0.28.17 || ^0.29.0", + "nanostores": "^1.1.1", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@lynx-js/react": "*", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "@sveltejs/kit": "^2.0.0", + "@tanstack/react-start": "^1.0.0", + "@tanstack/solid-start": "^1.0.0", + "better-sqlite3": "^12.0.0", + "drizzle-kit": ">=0.31.4", + "drizzle-orm": "^0.45.2", + "mongodb": "^6.0.0 || ^7.0.0", + "mysql2": "^3.0.0", + "next": "^14.0.0 || ^15.0.0 || ^16.0.0", + "pg": "^8.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "solid-js": "^1.0.0", + "svelte": "^4.0.0 || ^5.0.0", + "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "@lynx-js/react": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "@tanstack/react-start": { + "optional": true + }, + "@tanstack/solid-start": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "drizzle-kit": { + "optional": true + }, + "drizzle-orm": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "next": { + "optional": true + }, + "pg": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vitest": { + "optional": true + }, + "vue": { + "optional": true + } + } }, - "node_modules/body-parser": { - "version": "1.20.5", + "node_modules/better-call": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.7.tgz", + "integrity": "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w==", "license": "MIT", - "optional": true, "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", + "@better-auth/utils": "^0.4.0", + "@better-fetch/fetch": "^1.1.21", + "rou3": "^0.7.12", + "set-cookie-parser": "^3.0.1" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "license": "MIT", + "optional": true, + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", @@ -3191,7 +3580,7 @@ }, "node_modules/bun-types": { "version": "1.3.14", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -3256,6 +3645,56 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", @@ -3295,6 +3734,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "license": "MIT", @@ -3468,7 +3917,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3489,6 +3937,25 @@ "node": ">=0.10.0" } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -3503,6 +3970,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "license": "MIT", @@ -3514,11 +3990,25 @@ }, "node_modules/detect-libc": { "version": "2.1.2", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "dev": true, @@ -4283,7 +4773,7 @@ }, "node_modules/esbuild": { "version": "0.28.1", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -4334,6 +4824,28 @@ "license": "MIT", "optional": true }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "dev": true, @@ -4422,8 +4934,15 @@ "license": "MIT", "optional": true }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -4541,6 +5060,7 @@ "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, @@ -4660,6 +5180,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "dev": true, @@ -4675,6 +5235,16 @@ "node": ">=16.9.0" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-errors": { "version": "2.0.1", "license": "MIT", @@ -4737,6 +5307,12 @@ "version": "2.0.4", "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "license": "MIT", @@ -4745,6 +5321,40 @@ "node": ">= 0.10" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-electron": { "version": "2.2.2", "license": "MIT" @@ -4756,6 +5366,28 @@ "node": ">=8" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "2.0.1", "license": "MIT", @@ -4766,6 +5398,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jose": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", + "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "dev": true, @@ -4810,8 +5451,18 @@ "dev": true, "license": "MIT" }, + "node_modules/kysely": { + "version": "0.28.17", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz", + "integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/lightningcss": { "version": "1.33.0", + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -4844,6 +5495,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4864,6 +5516,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4884,6 +5537,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4904,6 +5558,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4924,6 +5579,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -4944,6 +5600,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -4967,6 +5624,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -4988,6 +5646,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -5009,6 +5668,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -5032,6 +5692,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5052,6 +5713,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5079,6 +5741,16 @@ "version": "4.0.1", "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "dev": true, @@ -5087,44 +5759,887 @@ "yallist": "^3.0.2" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "dev": true, + "node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "micromark-util-types": "^2.0.0" } }, - "node_modules/media-typer": { - "version": "0.3.0", + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/merge-descriptors": { - "version": "1.0.3", + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/methods": { - "version": "1.1.2", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, "node_modules/mime": { "version": "1.6.0", @@ -5175,11 +6690,11 @@ }, "node_modules/ms": { "version": "2.1.3", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.16", + "dev": true, "funding": [ { "type": "github", @@ -5194,6 +6709,21 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanostores": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.4.2.tgz", + "integrity": "sha512-Wxv8Roefr2nqtiRG0bnaFlpYqpIVtOEeJZHaH+4nGgOK1/7n6OHOuHCb/bhqrNQgZM8fyd0s1PqhdrJc9Ib44g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/negotiator": { "version": "0.6.3", "license": "MIT", @@ -5336,6 +6866,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "license": "MIT", @@ -5397,10 +6952,12 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.5", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5411,6 +6968,7 @@ }, "node_modules/postcss": { "version": "8.5.25", + "dev": true, "funding": [ { "type": "opencollective", @@ -5497,6 +7055,16 @@ } } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "license": "MIT", @@ -5580,6 +7148,99 @@ "react": "^19.2.8" } }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "license": "MIT", @@ -5608,6 +7269,7 @@ }, "node_modules/rolldown": { "version": "1.1.5", + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.139.0", @@ -5744,6 +7406,12 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "license": "ISC" @@ -5888,6 +7556,7 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -5910,6 +7579,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stack-trace": { "version": "1.0.0", "dev": true, @@ -5937,6 +7616,20 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "license": "MIT", @@ -5947,6 +7640,24 @@ "node": ">=8" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/supports-color": { "version": "10.2.2", "dev": true, @@ -5970,6 +7681,7 @@ }, "node_modules/tinyglobby": { "version": "0.2.17", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -5989,10 +7701,31 @@ "node": ">=0.6" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "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 }, @@ -6005,7 +7738,7 @@ }, "node_modules/tsx": { "version": "4.23.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.28.0" @@ -6049,8 +7782,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6079,6 +7812,93 @@ "pathe": "^2.0.3" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unpipe": { "version": "1.0.0", "license": "MIT", @@ -6140,8 +7960,37 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "8.1.5", + "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -6235,6 +8084,12 @@ "version": "2.0.1", "license": "ISC" }, + "node_modules/wildcard-match": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.4.tgz", + "integrity": "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==", + "license": "ISC" + }, "node_modules/workerd": { "version": "1.20260722.1", "dev": true, @@ -7008,7 +8863,7 @@ }, "node_modules/ws": { "version": "8.21.0", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -7136,6 +8991,16 @@ "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index c693d56..9cbad30 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@preact/preset-vite": "^2.10.6", "@types/bun": "latest", "drizzle-kit": "^0.31.10", + "typescript": "^5.9.3", "vite": "^8.1.5", "wrangler": "^4.113.0" }, @@ -34,11 +35,13 @@ "@orpc/client": "^1.14.7", "@orpc/openapi": "^1.14.7", "@orpc/server": "^1.14.7", + "@orpc/zod": "^1.14.7", "@preact/signals": "^2.10.0", "@slack/events-api": "^3.0.1", "@slack/web-api": "^7.18.0", "@vitejs/plugin-react": "^6.0.4", "@xyflow/react": "^12.11.2", + "better-auth": "^1.6.25", "drizzle-orm": "^0.45.2", "feed": "^5.2.1", "hono": "^4.12.30", diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..23a26f4 --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,10 @@ +import { createORPCClient } from "@orpc/client"; +import { RPCLink } from "@orpc/client/fetch"; +import type { RouterClient } from "@orpc/server"; +import type { AppRouter } from "./router"; + +const link = new RPCLink({ + url: "/api", +}); + +export const orpc: RouterClient = createORPCClient(link); diff --git a/src/api/context.ts b/src/api/context.ts new file mode 100644 index 0000000..0f398d2 --- /dev/null +++ b/src/api/context.ts @@ -0,0 +1,60 @@ +import { ORPCError } from "@orpc/server"; +import { os } from "@orpc/server"; +import type { Store } from "../store/store"; +import type { ApiKeyIdentity } from "../lib/api-keys"; + +export interface SessionIdentity { + user: { + id: string; + name: string; + email: string; + image?: string | null; + slackId?: string | null; + }; +} + +export interface ORPCContext { + store: Store; + session: SessionIdentity | null; + apiKey: ApiKeyIdentity | null; + databaseUrl: string; +} + +const base = os.$context(); + +export const publicProcedure = base; + +export const authRequiredProcedure = base.use(async ({ context, next }) => { + if (!context.session) { + throw new ORPCError("UNAUTHORIZED", { + message: "Authentication required", + }); + } + return next({ + context: { + session: context.session, + }, + }); +}); + +export const apiKeyRequiredProcedure = base.use(async ({ context, next }) => { + if (!context.apiKey) { + throw new ORPCError("UNAUTHORIZED", { + message: "API key required", + }); + } + return next({ + context: { + apiKey: context.apiKey, + }, + }); +}); + +export const authOrApiKeyProcedure = base.use(async ({ context, next }) => { + if (!context.session && !context.apiKey) { + throw new ORPCError("UNAUTHORIZED", { + message: "Authentication or API key required", + }); + } + return next(); +}); diff --git a/src/api/router.ts b/src/api/router.ts index 2954bff..d8e6c7c 100644 --- a/src/api/router.ts +++ b/src/api/router.ts @@ -1,90 +1,13 @@ -import { z } from "zod"; -import type { Store, StoreMessage } from "../store/store"; - -const listInput = z.object({ - channel: z.string().min(1, "channel is required"), - after: z.string().optional(), - before: z.string().optional(), - userId: z.string().optional(), - limit: z.coerce.number().int().min(1).max(10000).default(50), - page: z.coerce.number().int().min(1).default(1), -}); - -const getInput = z.object({ - slackTs: z.string(), - channel: z.string().min(1, "channel is required"), -}); - -export async function handleAPI(store: Store, url: URL, req: Request): Promise { - const path = url.pathname; - const method = req.method; - - if (path === "/api/messages" && method === "GET") { - const parsed = listInput.safeParse(Object.fromEntries(url.searchParams)); - if (!parsed.success) { - return new Response(JSON.stringify({ error: parsed.error.flatten().fieldErrors }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); - } - - const { channel, after, before, userId, limit, page } = parsed.data; - - const ch = await store.getChannel(channel); - if (!ch || !ch.enabled) { - return new Response(JSON.stringify({ error: "channel not found or not enabled" }), { - status: 404, - headers: { "Content-Type": "application/json" }, - }); - } - - const all = await store.getMessages(channel, 100000, 0); - - let filtered: StoreMessage[] = all; - if (after) filtered = filtered.filter((m) => m.timestamp >= after); - if (before) filtered = filtered.filter((m) => m.timestamp <= before); - if (userId) filtered = filtered.filter((m) => m.userId === userId); - - const total = filtered.length; - const offset = (page - 1) * limit; - const items = filtered.slice(offset, offset + limit); - - return new Response( - JSON.stringify({ - data: items, - pagination: { page, limit, total, total_pages: Math.ceil(total / limit) }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - - const msgMatch = path.match(/^\/api\/messages\/([^/]+)$/); - if (msgMatch && method === "GET") { - const slackTs = decodeURIComponent(msgMatch[1]); - const channel = url.searchParams.get("channel") || ""; - - const parsed = getInput.safeParse({ slackTs, channel }); - if (!parsed.success) { - return new Response(JSON.stringify({ error: parsed.error.flatten().fieldErrors }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); - } - - const all = await store.getMessages(channel, 100000, 0); - const msg = all.find((m) => m.slackTs === slackTs); - if (!msg) { - return new Response(JSON.stringify({ error: "message not found" }), { - status: 404, - headers: { "Content-Type": "application/json" }, - }); - } - - return new Response(JSON.stringify({ data: msg }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } - - return null; // not an API route -} +import { channelRouter } from "./routers/channels"; +import { messageRouter } from "./routers/messages"; +import { subscriptionRouter } from "./routers/subscriptions"; +import { apiKeyRouter } from "./routers/apiKeys"; + +export const router = { + channels: channelRouter, + messages: messageRouter, + subscriptions: subscriptionRouter, + apiKeys: apiKeyRouter, +}; + +export type AppRouter = typeof router; diff --git a/src/api/routers/apiKeys.ts b/src/api/routers/apiKeys.ts new file mode 100644 index 0000000..286fc38 --- /dev/null +++ b/src/api/routers/apiKeys.ts @@ -0,0 +1,84 @@ +import { ORPCError } from "@orpc/server"; +import { z } from "zod"; +import { authRequiredProcedure } from "../context"; +import { generateApiKey } from "../../lib/api-keys"; +import { getDb } from "../../db"; +import { apiKeys, apiKeyChannels } from "../../db/schema"; +import { eq } from "drizzle-orm"; + +export const listApiKeys = authRequiredProcedure + .route({ method: "GET", path: "/api-keys" }) + .handler(async ({ context }) => { + const db = getDb(context.databaseUrl); + const userId = context.session?.user?.id; + if (!userId) return []; + + const keys = await db + .select({ + id: apiKeys.id, + name: apiKeys.name, + keyPrefix: apiKeys.keyPrefix, + createdAt: apiKeys.createdAt, + lastUsedAt: apiKeys.lastUsedAt, + }) + .from(apiKeys) + .where(eq(apiKeys.owner, userId)); + + const keysWithChannels = await Promise.all( + keys.map(async (key) => { + const channels = await db + .select({ channelId: apiKeyChannels.channelId }) + .from(apiKeyChannels) + .where(eq(apiKeyChannels.keyId, key.id)); + return { ...key, channels: channels.map((c) => c.channelId) }; + }) + ); + + return keysWithChannels; + }); + +export const createApiKey = authRequiredProcedure + .route({ method: "POST", path: "/api-keys" }) + .input(z.object({ name: z.string().min(1), channelIds: z.array(z.string()).optional() })) + .handler(async ({ input, context }) => { + const { fullKey, keyPrefix, secretHash } = generateApiKey(); + const hash = await secretHash; + + const db = getDb(context.databaseUrl); + const [keyRow] = await db + .insert(apiKeys) + .values({ + name: input.name, + owner: context.session?.user?.id || null, + keyPrefix, + secretHash: hash, + }) + .returning({ id: apiKeys.id }); + + if (input.channelIds && keyRow) { + for (const channelId of input.channelIds) { + await db.insert(apiKeyChannels).values({ + keyId: keyRow.id, + channelId, + }); + } + } + + return { fullKey, keyPrefix, name: input.name, id: keyRow?.id }; + }); + +export const revokeApiKey = authRequiredProcedure + .route({ method: "DELETE", path: "/api-keys/{id}" }) + .input(z.object({ id: z.coerce.number().int() })) + .handler(async ({ input, context }) => { + const db = getDb(context.databaseUrl); + await db.delete(apiKeyChannels).where(eq(apiKeyChannels.keyId, input.id)); + await db.update(apiKeys) .set({ revokedBy: context.session?.user?.id || null }).where(eq(apiKeys.id, input.id)); + return {ok: true} + }); + +export const apiKeyRouter = { + list: listApiKeys, + create: createApiKey, + revoke: revokeApiKey, +}; diff --git a/src/api/routers/channels.ts b/src/api/routers/channels.ts new file mode 100644 index 0000000..2fe0aed --- /dev/null +++ b/src/api/routers/channels.ts @@ -0,0 +1,70 @@ +import { ORPCError } from "@orpc/server"; +import { z } from "zod"; +import { publicProcedure, authOrApiKeyProcedure, authRequiredProcedure } from "../context"; + +const channelSchema = z.object({ + id: z.string(), + name: z.string(), + teamId: z.string(), + enabled: z.boolean(), + linkMode: z.boolean(), + webhookUrl: z.string(), + autoApproveUsers: z.array(z.string()), + approvedPosters: z.array(z.string()), + accessPermUsers: z.array(z.string()), + trackReplies: z.boolean(), + metadataSchema: z.string(), + createdAt: z.string(), +}); + +export const listChannels = publicProcedure + .route({ method: "GET", path: "/channels" }) + .input( + z.object({ + enabled: z.coerce.boolean().optional(), + }), + ) + .handler(async ({ input, context }) => { + const channels = input.enabled + ? await context.store.listEnabledChannels() + : await context.store.listChannels(); + return channels; + }); + +export const getChannel = publicProcedure + .route({ method: "GET", path: "/channels/{id}" }) + .input(z.object({ id: z.string().min(1) })) + .handler(async ({ input, context }) => { + const ch = await context.store.getChannel(input.id); + if (!ch) throw new ORPCError("Channel not found"); + return ch; + }); + +export const upsertChannel = authRequiredProcedure + .route({ method: "PUT", path: "/channels" }) + .input(channelSchema) + .handler(async ({ input, context }) => { + await context.store.upsertChannel(input); + }); + +export const listByUser = authOrApiKeyProcedure + .route({ method: "GET", path: "/channels/by-user/{userId}" }) + .input(z.object({ userId: z.string().min(1) })) + .handler(async ({ input, context }) => { + const slackId = context.session?.user?.slackId; + if (!slackId) return []; + const channels = await context.store.listChannels(); + return channels.filter( + (ch) => + ch.accessPermUsers.includes(slackId) || + ch.accessPermUsers.includes("*") || + ch.accessPermUsers.includes(slackId), + ); + }); + +export const channelRouter = { + list: listChannels, + get: getChannel, + upsert: upsertChannel, + listByUser, +}; diff --git a/src/api/routers/messages.ts b/src/api/routers/messages.ts new file mode 100644 index 0000000..41c757d --- /dev/null +++ b/src/api/routers/messages.ts @@ -0,0 +1,58 @@ +import { ORPCError } from "@orpc/server"; +import { z } from "zod"; +import { authOrApiKeyProcedure } from "../context"; + +export const listMessages = authOrApiKeyProcedure + .route({ method: "GET", path: "/messages" }) + .input( + z.object({ + channel: z.string().min(1), + after: z.string().optional(), + before: z.string().optional(), + userId: z.string().optional(), + limit: z.coerce.number().int().min(1).max(10000).default(50), + page: z.coerce.number().int().min(1).default(1), + }), + ) + .handler(async ({ input, context }) => { + const { channel, after, before, userId, limit, page } = input; + + const ch = await context.store.getChannel(channel); + if (!ch || !ch.enabled) throw new ORPCError("Channel not found or not enabled"); + + const all = await context.store.getMessages(channel, 100000, 0); + + let filtered = all; + if (after) filtered = filtered.filter((m: any) => m.timestamp >= after); + if (before) filtered = filtered.filter((m: any) => m.timestamp <= before); + if (userId) filtered = filtered.filter((m: any) => m.userId === userId); + + const total = filtered.length; + const offset = (page - 1) * limit; + const items = filtered.slice(offset, offset + limit); + + return { + data: items, + pagination: { page, limit, total, total_pages: Math.ceil(total / limit) }, + }; + }); + +export const getMessage = authOrApiKeyProcedure + .route({ method: "GET", path: "/messages/{slackTs}" }) + .input( + z.object({ + slackTs: z.string(), + channel: z.string().min(1), + }), + ) + .handler(async ({ input, context }) => { + const all = await context.store.getMessages(input.channel, 100000, 0); + const msg = all.find((m: any) => m.slackTs === input.slackTs); + if (!msg) throw new ORPCError("Message not found"); + return msg; + }); + +export const messageRouter = { + list: listMessages, + get: getMessage, +}; diff --git a/src/api/routers/subscriptions.ts b/src/api/routers/subscriptions.ts new file mode 100644 index 0000000..c532168 --- /dev/null +++ b/src/api/routers/subscriptions.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; +import { ORPCError } from "@orpc/server"; +import { publicProcedure, authOrApiKeyProcedure, authRequiredProcedure } from "../context"; + +export const listSubscriptions = publicProcedure + .route({ method: "GET", path: "/subscriptions" }) + .input(z.object({ sourceChannelId: z.string().min(1) })) + .handler(async ({ input, context }) => { + return context.store.getSubscribersBySource(input.sourceChannelId); + }); + +export const listSubscriptionsBySubscriber = publicProcedure + .route({ method: "GET", path: "/subscriptions/by-subscriber" }) + .input(z.object({ subscriberChannelId: z.string().min(1) })) + .handler(async ({ input, context }) => { + return context.store.getSubscriptionsBySubscriber(input.subscriberChannelId); + }); + +export const addSubscription = authRequiredProcedure + .route({ method: "POST", path: "/subscriptions" }) + .input( + z.object({ + subscriberChannelId: z.string().min(1), + sourceChannelId: z.string().min(1), + }), + ) + .handler(async ({ input, context }) => { + const source = await context.store.getChannel(input.sourceChannelId); + if (!source) throw new ORPCError("NOT_FOUND", { message: `Source channel ${input.sourceChannelId} not found` }); + const sub = await context.store.getChannel(input.subscriberChannelId); + if (!sub) throw new ORPCError("NOT_FOUND", { message: `Subscriber channel ${input.subscriberChannelId} not found` }); + await context.store.addSubscription(input.subscriberChannelId, input.sourceChannelId); + }); + +export const removeSubscription = authRequiredProcedure + .route({ method: "DELETE", path: "/subscriptions" }) + .input( + z.object({ + subscriberChannelId: z.string().min(1), + sourceChannelId: z.string().min(1), + }), + ) + .handler(async ({ input, context }) => { + await context.store.removeSubscription(input.subscriberChannelId, input.sourceChannelId); + }); + +export const subscriptionRouter = { + list: listSubscriptions, + listBySubscriber: listSubscriptionsBySubscriber, + add: addSubscription, + remove: removeSubscription, +}; diff --git a/src/app.ts b/src/app.ts index af71b61..562ef55 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,10 +1,26 @@ import { Hono } from "hono"; import { WebClient } from "@slack/web-api"; +import { getAuth } from "./lib/auth" import { Feed } from "feed"; import type { Store, StoreChannel, StoreMessage, StoreSubscription } from "./store/store"; +import { OpenAPIHandler } from "@orpc/openapi/fetch"; +import { OpenAPIGenerator } from "@orpc/openapi"; +import { RPCHandler } from "@orpc/server/fetch"; +import { ZodSmartCoercionPlugin } from "@orpc/zod"; +import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4"; +import { router } from "./api/router"; +import type { ORPCContext } from "./api/context"; +import { createApiKeyContext } from "./lib/api-keys"; +import { getDb } from "./db"; +import * as schema from "./db/schema"; +import { eq } from "drizzle-orm"; // --- Helpers --- +function getDbUrl(env: Env): string { + return env.HYPERDRIVE_BINDING?.connectionString || env.DATABASE_URL; +} + function slackTsToTime(ts: string): Date { const parts = ts.split("."); const sec = parseInt(parts[0] || "0") || 0; @@ -167,6 +183,51 @@ function isSlackPermalink(text: string): boolean { /^https:\/\/[a-z0-9-]+\.slack\.com\/archives\/[A-Z0-9]+\/p\d+$/.test(trimmed); } +// --- Feed access control --- + +interface FeedIdentity { + type: "session" | "apikey"; + slackId: string; +} + +async function resolveFeedIdentity(c: any, env: Env): Promise { + const dbUrl = getDbUrl(env); + + // 1. Try session from cookie + try { + const auth = getAuth(dbUrl, env as any); + const s = await auth.api.getSession({ headers: c.req.raw.headers }); + if (s?.user && (s.user as any).slackId) { + return { type: "session", slackId: (s.user as any).slackId }; + } + } catch {} + + // 2. Try API key from header or ?api_key= param + const authHeader = c.req.header("Authorization") || ""; + const queryKey = c.req.query("api_key") || ""; + const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : queryKey; + if (token && token.startsWith("ind_")) { + try { + const identity = await createApiKeyContext({ req: c.req.raw, databaseUrl: dbUrl }); + if (identity.owner) { + const db = getDb(dbUrl); + const users = await db.select({ slackId: schema.authUser.slackId }).from(schema.authUser).where(eq(schema.authUser.id, identity.owner)); + if (users[0]?.slackId) { + return { type: "apikey", slackId: users[0].slackId }; + } + } + } catch {} + } + + return null; +} + +function canAccessFeed(ch: StoreChannel, identity: FeedIdentity | null): boolean { + if (ch.accessPermUsers.includes("*")) return true; + if (!identity) return false; + return ch.accessPermUsers.includes(identity.slackId); +} + async function forwardToSubscribers( msg: StoreMessage, sourceChannel: StoreChannel, @@ -305,22 +366,21 @@ async function forwardToSubscribers( try { if (sourceChannel.linkMode) { - // Keep the raw permalink in the top-level text so Slack generates its unfurl. - const linkBlocks: any[] = []; - if (metadataSection) { - linkBlocks.push({ type: "section", text: { type: "mrkdwn", text: metadataSection.trim() } }); - } - linkBlocks.push({ type: "section", text: { type: "mrkdwn", text: footerText } }); const res = await slack.chat.postMessage({ channel: sub.subscriberChannelId, username: postUsername, icon_url: postIcon, - unfurl_links: !metadataSection, - unfurl_media: !metadataSection, + unfurl_links: true, + unfurl_media: true, text: metadataSection - ? metadataSection.trim() + ? metadataSection.trim() + "\n" + footerText : (forwardLink || footerText), - blocks: linkBlocks, + blocks: metadataSection + ? [ + { type: "section", text: { type: "mrkdwn", text: metadataSection.trim() } }, + { type: "section", text: { type: "mrkdwn", text: footerText } }, + ] + : undefined, } as any); if (res?.ts) { await store.addBotAction({ type: "pub", sourceChannelId: msg.channelId, sourceMessageTs: msg.slackTs, botChannelId: sub.subscriberChannelId, botMessageTs: res.ts, createdAt: "" }); @@ -373,47 +433,154 @@ async function forwardToSubscribers( const app = new Hono<{ Bindings: Env }>(); -// Public site configuration used by the static frontend. -app.get("/site-config", async (c) => { - const env = c.env; - const ids = (env.LOCKDOWN_USERS || "").split(",").map((id) => id.trim()).filter(Boolean); - if (ids.length === 0) return c.json({ lockdownUsers: [] }); +// --- oRPC --- +const openAPIHandler = new OpenAPIHandler(router, { + plugins: [new ZodSmartCoercionPlugin()], +}); - const slack = new WebClient(env.SLACK_BOT_TOKEN); - const lockdownUsers = await Promise.all(ids.map(async (id) => { - let name = id; - try { - const response = await slack.users.info({ user: id }); - const user = response.user as any; - name = user?.profile?.display_name || user?.name || user?.real_name || id; - } catch {} - return { - id, - name, - url: `https://hackclub.enterprise.slack.com/team/${id}`, - }; - })); - - return c.json({ lockdownUsers }); +const rpcHandler = new RPCHandler(router, { + plugins: [new ZodSmartCoercionPlugin()], +}); + +const openAPIGenerator = new OpenAPIGenerator({ + schemaConverters: [new ZodToJsonSchemaConverter()], }); -// Middleware: Basic Auth for /api routes app.use("/api/*", async (c, next) => { - const env = c.env; - const apiPassword = env.API_PASSWORD || ""; - if (!apiPassword) return next(); - const apiUsername = env.API_USERNAME || "admin"; - const header = c.req.header("Authorization") || ""; - const match = header.match(/^Basic\s+(.+)$/); - if (!match) { - return c.text("Unauthorized", 401, { "WWW-Authenticate": "Basic realm=indigest" }); + if (c.req.path.startsWith("/api/auth/")) return next(); + + const dbUrl = getDbUrl(c.env); + const store = await getStore(dbUrl); + + let session: ORPCContext["session"] = null; + let apiKey: ORPCContext["apiKey"] = null; + + // Resolve session from cookies + try { + const auth = getAuth(dbUrl, c.env as any); + const s = await auth.api.getSession({ headers: c.req.raw.headers }); + if (s?.user) { + session = { user: s.user as any }; + } + } catch (err: any) { + console.error("Session resolution error:", err.message); } - const decoded = atob(match[1]!); - const [user, pass] = decoded.split(":"); - if (user !== apiUsername || pass !== apiPassword) { - return c.text("Unauthorized", 401, { "WWW-Authenticate": "Basic realm=indigest" }); + + // Resolve API key from Bearer token (if present) + const authHeader = c.req.header("Authorization") || ""; + if (authHeader.startsWith("Bearer ")) { + try { + apiKey = await createApiKeyContext({ req: c.req.raw, databaseUrl: dbUrl }); + } catch {} } - return next(); + + const ctx: ORPCContext = { store, session, apiKey, databaseUrl: dbUrl }; + + const { matched: openMatched, response: openResponse } = await openAPIHandler.handle(c.req.raw, { + prefix: "/api", + context: ctx, + }); + if (openMatched) return c.newResponse(openResponse.body, openResponse); + + const { matched: rpcMatched, response: rpcResponse } = await rpcHandler.handle(c.req.raw, { + context: ctx, + prefix: "/api", + }); + if (rpcMatched) return c.newResponse(rpcResponse.body, rpcResponse); + await next(); +}); + +app.get("/docs", (c) => { + const html = ` + + + Indigest API + + + + +
+ + + +`; + return c.html(html); +}); + +let authDbReady: Record> = {}; +app.on(["POST", "GET"], "/api/auth/*", async (c) => { + try { + const dbUrl = getDbUrl(c.env); + if (!authDbReady[dbUrl]) { + authDbReady[dbUrl] = (async () => { + const { pushSchema } = await import("./db/migrate"); + await pushSchema(dbUrl); + })(); + } + await authDbReady[dbUrl]; + const auth = getAuth(dbUrl, c.env as any); + return auth.handler(c.req.raw); + } catch (err: any) { + console.error("Auth handler error:", err.message, err.stack); + return c.json({ error: "auth handler failed" }, 500); + } +}); + + +// GitHub stars cache +let cachedStars: number | null = null; +let starsFetchedAt = 0; +const STARS_CACHE_TTL = 10 * 60 * 1000; // 10 minutes + +async function getGithubStars(): Promise { + if (cachedStars !== null && Date.now() - starsFetchedAt < STARS_CACHE_TTL) return cachedStars; + try { + const res = await fetch("https://api.github.com/repos/matmanna/indigest", { + headers: { "User-Agent": "indigest-bot", "Accept": "application/vnd.github.v3+json" }, + }); + const data = (await res.json()) as any; + if (typeof data.stargazers_count === "number") { + cachedStars = data.stargazers_count; + starsFetchedAt = Date.now(); + } + } catch {} + if (cachedStars !== null) return cachedStars; + return 0; +} + +// Public site configuration used by the static frontend. +app.get("/site-config", async (c) => { + c.header("Cache-Control", "no-store"); + const env = c.env; + const ids = (env.LOCKDOWN_USERS || "").split(",").map((id) => id.trim()).filter(Boolean); + const [lockdownUsers, githubStars] = await Promise.all([ + ids.length === 0 ? Promise.resolve([]) : (async () => { + const slack = new WebClient(env.SLACK_BOT_TOKEN); + return Promise.all(ids.map(async (id) => { + let name = id; + try { + const response = await slack.users.info({ user: id }); + const user = response.user as any; + name = user?.profile?.display_name || user?.name || user?.real_name || id; + } catch {} + return { + id, + name, + url: `https://hackclub.enterprise.slack.com/team/${id}`, + }; + })); + })(), + getGithubStars(), + ]); + + return c.json({ lockdownUsers, githubStars }); }); // === Scalar API Docs === @@ -421,18 +588,20 @@ app.get("/spec.json", (c) => { const spec = { openapi: "3.1.0", info: { - title: "indigest", + title: "Indigest API", version: "1.0.0", description: "A Slack bot that curates messages into digestible feeds", }, servers: [{ url: "/" }], + security: [{ BearerAuth: [] }], paths: { "/feed/{channelId}": { get: { operationId: "getFeed", summary: "Get channel feed", - description: "Returns an RSS feed or JSON feed of messages for a channel", + description: "Returns an RSS feed or JSON feed of messages for a channel. Requires auth if accessPermUsers is not *.", tags: ["Feed"], + security: [{ BearerAuth: [] }, { session: [] }], parameters: [ { name: "channelId", @@ -451,11 +620,20 @@ app.get("/spec.json", (c) => { in: "query", schema: { type: "integer", default: 0 }, }, + { + name: "api_key", + in: "query", + schema: { type: "string" }, + description: "API key (alternative to Authorization header)", + }, ], responses: { "200": { description: "RSS feed (application/rss+xml) or JSON array depending on Accept header or .json suffix", }, + "403": { + description: "Forbidden — no access to this feed", + }, }, }, }, @@ -463,9 +641,9 @@ app.get("/spec.json", (c) => { get: { operationId: "listMessages", summary: "List messages", - description: "List messages for a channel with filtering and pagination", + description: "List messages for a channel with filtering and pagination. Requires auth if accessPermUsers is not *.", tags: ["Messages"], - security: [{ basicAuth: [] }], + security: [{ BearerAuth: [] }, { session: [] }], parameters: [ { name: "channel", @@ -492,6 +670,12 @@ app.get("/spec.json", (c) => { schema: { type: "string" }, description: "Filter by Slack user ID", }, + { + name: "threadTs", + in: "query", + schema: { type: "string" }, + description: "Filter by thread parent timestamp", + }, { name: "limit", in: "query", @@ -528,9 +712,9 @@ app.get("/spec.json", (c) => { get: { operationId: "getMessage", summary: "Get message", - description: "Get a single message by its Slack timestamp", - tags: ["API"], - security: [{ basicAuth: [] }], + description: "Get a single message by its Slack timestamp. Requires auth if accessPermUsers is not *.", + tags: ["Messages"], + security: [{ BearerAuth: [] }, { session: [] }], parameters: [ { name: "slackTs", @@ -627,12 +811,162 @@ app.get("/spec.json", (c) => { }, }, }, + "/api/channels/by-user/{userId}": { + get: { + operationId: "listChannelsByUser", + summary: "List channels by user", + description: "List channels the user has access to based on accessPermUsers.", + tags: ["Channels"], + security: [{ BearerAuth: [] }, { session: [] }], + parameters: [ + { + name: "userId", + in: "path", + required: true, + schema: { type: "string" }, + description: "Better Auth user ID", + }, + ], + responses: { + "200": { + description: "Channels the user can access", + content: { + "application/json": { + schema: { + type: "array", + items: { $ref: "#/components/schemas/Channel" }, + }, + }, + }, + }, + }, + }, + }, + "/api/subscriptions": { + get: { + operationId: "listSubscriptions", + summary: "List subscriptions", + description: "List all subscriber channels for a given source channel.", + tags: ["Subscriptions"], + parameters: [ + { + name: "sourceChannelId", + in: "query", + required: true, + schema: { type: "string" }, + description: "Source Slack channel ID", + }, + ], + responses: { + "200": { + description: "List of subscriptions", + content: { + "application/json": { + schema: { + type: "array", + items: { $ref: "#/components/schemas/Subscription" }, + }, + }, + }, + }, + }, + }, + post: { + operationId: "addSubscription", + summary: "Add subscription", + description: "Subscribe a channel to a source channel.", + tags: ["Subscriptions"], + security: [{ BearerAuth: [] }, { session: [] }], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + required: ["subscriberChannelId", "sourceChannelId"], + properties: { + subscriberChannelId: { type: "string", description: "Channel to receive forwards" }, + sourceChannelId: { type: "string", description: "Channel to forward from" }, + }, + }, + }, + }, + }, + responses: { + "200": { description: "Subscription created" }, + }, + }, + delete: { + operationId: "removeSubscription", + summary: "Remove subscription", + description: "Unsubscribe a channel from a source channel.", + tags: ["Subscriptions"], + security: [{ BearerAuth: [] }, { session: [] }], + parameters: [ + { + name: "subscriberChannelId", + in: "query", + required: true, + schema: { type: "string" }, + description: "Channel to unsubscribe", + }, + { + name: "sourceChannelId", + in: "query", + required: true, + schema: { type: "string" }, + description: "Source channel to unsubscribe from", + }, + ], + responses: { + "200": { description: "Subscription removed" }, + }, + }, + }, + "/api/subscriptions/by-subscriber": { + get: { + operationId: "listSubscriptionsBySubscriber", + summary: "List subscriptions by subscriber", + description: "List all source channels a subscriber channel is subscribed to.", + tags: ["Subscriptions"], + parameters: [ + { + name: "subscriberChannelId", + in: "query", + required: true, + schema: { type: "string" }, + description: "Subscriber Slack channel ID", + }, + ], + responses: { + "200": { + description: "List of subscriptions", + content: { + "application/json": { + schema: { + type: "array", + items: { $ref: "#/components/schemas/Subscription" }, + }, + }, + }, + }, + }, + }, + }, }, components: { securitySchemes: { - basicAuth: { + BearerAuth: { type: "http", - scheme: "basic", + scheme: "bearer", + bearerFormat: "API Key", + description: "API key (ind_...)", + }, + session: { + type: "apiKey", + in: "cookie", + name: "better-auth.session_token", + description: "Session cookie (browser login)", }, }, schemas: { @@ -673,6 +1007,15 @@ app.get("/spec.json", (c) => { total_pages: { type: "integer" }, }, }, + Subscription: { + type: "object", + properties: { + id: { type: "integer" }, + subscriberChannelId: { type: "string" }, + sourceChannelId: { type: "string" }, + createdAt: { type: "string" }, + }, + }, }, }, }; @@ -699,7 +1042,7 @@ app.post("/events", async (c) => { c.executionCtx.waitUntil((async () => { console.log("Received Slack event:", payload.event?.type, "thread_ts:", payload.event?.thread_ts, "channel:", payload.event?.channel); const ev = payload.event; - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const slack = new WebClient(env.SLACK_BOT_TOKEN); if (ev.type === "member_joined_channel") { @@ -714,6 +1057,7 @@ app.post("/events", async (c) => { webhookUrl: "", autoApproveUsers: [], approvedPosters: [], + accessPermUsers: ["*"], trackReplies: false, metadataSchema: "", createdAt: "", @@ -904,7 +1248,7 @@ app.post("/interactions", async (c) => { if (cb.type === "block_actions") { const action = cb.actions?.[0]; if (action?.action_id === "indigest_metadata") { - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const slack = new WebClient(env.SLACK_BOT_TOKEN); const channelId = cb.channel?.id; const messageTs = action.value; @@ -950,7 +1294,7 @@ app.post("/interactions", async (c) => { }); c.executionCtx.waitUntil((async () => { - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const slack = new WebClient(env.SLACK_BOT_TOKEN); if (cb.type === "view_submission" && cb.view?.callback_id === "metadata_modal") { @@ -1054,7 +1398,7 @@ app.post("/interactions", async (c) => { // Also forward to any new subscribers that weren't captured in the initial forward await forwardToSubscribers({ slackTs: messageTs, channelId, userId: msg.user || "", userName, text: msg.text || "", timestamp: slackTsToTime(messageTs).toISOString(), metadata }, channel, store, slack); - if (channel.linkMode) await postPermalinkReply(client, channelId, messageTs); + // if (channel.linkMode) await postPermalinkReply(client, channelId, messageTs); // Update the bot's prompt to show approved state if (botMessageTs) { @@ -1101,7 +1445,7 @@ app.post("/interactions", async (c) => { } c.executionCtx.waitUntil((async () => { - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const slack = new WebClient(env.SLACK_BOT_TOKEN); const lockdownUsers = (env.LOCKDOWN_USERS || "").split(",").map((s: string) => s.trim()).filter(Boolean); @@ -1260,7 +1604,7 @@ app.post("/interactions", async (c) => { const savedMsg = { slackTs: messageTs, channelId, userId: msg.user || "", userName, text: msg.text || "", timestamp: slackTsToTime(messageTs).toISOString(), metadata: {} }; await fireWebhook(ch, savedMsg); await forwardToSubscribers(savedMsg, ch, store, slack); - if (ch.linkMode) await postPermalinkReply(slack, channelId, messageTs); + // if (ch.linkMode) await postPermalinkReply(slack, channelId, messageTs); // Update the bot's prompt to show approved state if (botMessageTs) { @@ -1362,9 +1706,9 @@ const slackSlashCommand = async (c: any) => { c.res = new Response(JSON.stringify({ response_type: "ephemeral", text: "⏳ Processing..." }), { status: 200, headers: c.res.headers }); c.executionCtx.waitUntil((async () => { - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const slack = new WebClient(env.SLACK_BOT_TOKEN); - const baseUrl = env.BASE_URL || "http://localhost:8080"; + const baseUrl = env.BASE_URL || "http://localhost:8787"; const lockdownUsers = (env.LOCKDOWN_USERS || "").split(",").map((s: string) => s.trim()).filter(Boolean); const channelRefMatch = text.match(/^(<#(\w+)(\|[^>]*)?>|#(\S+))\s*(.*)$/); @@ -1406,7 +1750,7 @@ const slackSlashCommand = async (c: any) => { name = (conv.channel as any)?.name || targetChannelId; } catch {} const auth = await slack.auth.test(); - ch = { id: targetChannelId, name, teamId: auth.team_id || "", enabled: false, linkMode: false, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], trackReplies: false, metadataSchema: "", createdAt: "" }; + ch = { id: targetChannelId, name, teamId: auth.team_id || "", enabled: false, linkMode: false, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], accessPermUsers: ["*"], trackReplies: false, metadataSchema: "", createdAt: "" }; await store.upsertChannel(ch); } @@ -1902,12 +2246,17 @@ app.get("/feed/:channelId", async (c) => { const rawId = c.req.param("channelId")!; const wantsJson = rawId.endsWith(".json") || c.req.header("accept")?.includes("application/json"); const channelId = wantsJson ? rawId.replace(/\.json$/, "") : rawId; - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const ch = await store.getChannel(channelId); if (!ch || !ch.enabled) { return c.json({ error: "not found", id: channelId }, 404); } + const identity = await resolveFeedIdentity(c, env); + if (!canAccessFeed(ch, identity)) { + return c.json({ error: "forbidden", message: "You don't have access to this feed" }, 403); + } + const limit = Math.min(parseInt(c.req.query("limit") || "50") || 50, 200); const offset = parseInt(c.req.query("offset") || "0") || 0; const msgs = await store.getMessages(channelId, limit, offset); @@ -1916,7 +2265,7 @@ app.get("/feed/:channelId", async (c) => { return c.json(msgs); } - const baseUrl = env.BASE_URL || "http://localhost:8080"; + const baseUrl = env.BASE_URL || "http://localhost:8787"; const feed = new Feed({ title: `#${ch.name} — indigest`, link: `${baseUrl}/feed/${channelId}`, @@ -1944,7 +2293,7 @@ app.get("/feed/:channelId", async (c) => { // === REST API === app.get("/api/messages", async (c) => { const env = c.env; - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const channel = c.req.query("channel"); if (!channel) { return c.json({ error: "channel is required" }, 400); @@ -1955,9 +2304,15 @@ app.get("/api/messages", async (c) => { return c.json({ error: "channel not found or not enabled" }, 404); } + const identity = await resolveFeedIdentity(c, env); + if (!canAccessFeed(ch, identity)) { + return c.json({ error: "forbidden", message: "You don't have access to this channel" }, 403); + } + const after = c.req.query("after"); const before = c.req.query("before"); const userId = c.req.query("userId"); + const threadTs = c.req.query("threadTs"); const limit = Math.min(parseInt(c.req.query("limit") || "50") || 50, 10000); const page = parseInt(c.req.query("page") || "1") || 1; @@ -1967,6 +2322,7 @@ app.get("/api/messages", async (c) => { if (after) filtered = filtered.filter((m) => m.timestamp >= after); if (before) filtered = filtered.filter((m) => m.timestamp <= before); if (userId) filtered = filtered.filter((m) => m.userId === userId); + if (threadTs) filtered = filtered.filter((m) => m.threadTs === threadTs); const total = filtered.length; const offset = (page - 1) * limit; @@ -1980,7 +2336,7 @@ app.get("/api/messages", async (c) => { app.get("/api/messages/:slackTs", async (c) => { const env = c.env; - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const slackTs = decodeURIComponent(c.req.param("slackTs")); const channel = c.req.query("channel") || ""; @@ -1988,6 +2344,16 @@ app.get("/api/messages/:slackTs", async (c) => { return c.json({ error: "channel is required" }, 400); } + const ch = await store.getChannel(channel); + if (!ch || !ch.enabled) { + return c.json({ error: "channel not found or not enabled" }, 404); + } + + const identity = await resolveFeedIdentity(c, env); + if (!canAccessFeed(ch, identity)) { + return c.json({ error: "forbidden", message: "You don't have access to this channel" }, 403); + } + const all = await store.getMessages(channel, 100000, 0); const msg = all.find((m) => m.slackTs === slackTs); if (!msg) { @@ -1999,14 +2365,14 @@ app.get("/api/messages/:slackTs", async (c) => { app.get("/api/channels", async (c) => { const env = c.env; - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const channels = await store.listChannels(); return c.json({ data: channels }); }); app.get("/api/channels/:id", async (c) => { const env = c.env; - const store = await getStore(env.DATABASE_URL); + const store = await getStore(getDbUrl(env)); const id = decodeURIComponent(c.req.param("id")); const ch = await store.getChannel(id); if (!ch) return c.json({ error: "channel not found" }, 404); @@ -2014,11 +2380,33 @@ app.get("/api/channels/:id", async (c) => { }); // --- Graph API (for React Flow diagram) --- +let cachedGraph: { data: any; at: number } | null = null; +const GRAPH_CACHE_TTL = 60_000; // 1 minute + app.get("/api/graph", async (c) => { const env = c.env; - const store = await getStore(env.DATABASE_URL); - const channels = await store.listEnabledChannels(); + if (cachedGraph && Date.now() - cachedGraph.at < GRAPH_CACHE_TTL) { + return c.json(cachedGraph.data); + } + + const store = await getStore(getDbUrl(env)); + + const { channels, subscriptions, subscriberChannels } = await store.getGraphData(); + + // Index channels by ID for fast lookup + const channelMap = new Map(); + for (const ch of channels) channelMap.set(ch.id, ch); + for (const ch of subscriberChannels) channelMap.set(ch.id, ch); + + // Index subscriptions by source channel + const subsBySource = new Map(); + for (const sub of subscriptions) { + const list = subsBySource.get(sub.sourceChannelId) || []; + list.push(sub); + subsBySource.set(sub.sourceChannelId, list); + } + const nodes: Array<{ id: string; type: string; label: string; data: Record }> = []; const edges: Array<{ id: string; source: string; target: string; label?: string; animated: boolean }> = []; @@ -2032,9 +2420,10 @@ app.get("/api/graph", async (c) => { nodes.push({ id: `wh:${ch.id}`, type: "feed", label: "Webhook", data: { feedType: "webhook", channelId: ch.id, url: ch.webhookUrl } }); edges.push({ id: `e-wh-${ch.id}`, source: `ch:${ch.id}`, target: `wh:${ch.id}`, animated: true }); } - const subscribers = await store.getSubscribersBySource(ch.id); - for (const sub of subscribers) { - const subCh = await store.getChannel(sub.subscriberChannelId); + + const subs = subsBySource.get(ch.id) || []; + for (const sub of subs) { + const subCh = channelMap.get(sub.subscriberChannelId); const subLabel = subCh ? `#${subCh.name || subCh.id}` : sub.subscriberChannelId; if (!nodes.find((n) => n.id === `sub:${sub.subscriberChannelId}`)) { nodes.push({ id: `sub:${sub.subscriberChannelId}`, type: "subscriber", label: subLabel, data: { channelId: sub.subscriberChannelId, name: subCh?.name, webhookUrl: subCh?.webhookUrl, metadataSchema: subCh?.metadataSchema, trackReplies: subCh?.trackReplies, approvedPosters: subCh?.approvedPosters, autoApproveUsers: subCh?.autoApproveUsers?.join(", ") || "", enabled: subCh?.enabled, linkMode: subCh?.linkMode, createdAt: subCh?.createdAt } }); @@ -2043,7 +2432,9 @@ app.get("/api/graph", async (c) => { } } - return c.json({ data: { nodes, edges } }); + const result = { data: { nodes, edges } }; + cachedGraph = { data: result, at: Date.now() }; + return c.json(result); }); // --- Store (lazy singleton per DB URL) --- @@ -2101,6 +2492,7 @@ function retryingStore(store: Store): Store { getRecentMessages: (channelId, since) => withRetry(() => store.getRecentMessages(channelId, since)), addBotAction: (action) => withRetry(() => store.addBotAction(action)), getBotActionsBySource: (sourceChannelId, sourceMessageTs) => withRetry(() => store.getBotActionsBySource(sourceChannelId, sourceMessageTs)), + getGraphData: () => withRetry(() => store.getGraphData()), close: () => store.close(), }; } diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..c00bdd2 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,10 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "./schema"; + +// postgres driver — direct TCP, works with this Orchard database. +// Each call creates a fresh client (no I/O reuse across Worker requests). +export function getDb(databaseUrl: string) { + const client = postgres(databaseUrl, { max: 1, onnotice: () => {} }); + return drizzle(client, { schema }); +} diff --git a/src/db/migrate.ts b/src/db/migrate.ts index 7de90d8..51a3b76 100644 --- a/src/db/migrate.ts +++ b/src/db/migrate.ts @@ -95,5 +95,100 @@ export async function pushSchema(connectionString: string) { CREATE INDEX IF NOT EXISTS idx_bot_actions_source ON bot_actions(source_channel_id, source_message_ts) `); + // --- Better Auth tables --- + + await db.execute(sql` + CREATE TABLE IF NOT EXISTS auth_user ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + email_verified BOOLEAN NOT NULL DEFAULT FALSE, + image TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() + ) + `); + + await db.execute(sql` + CREATE TABLE IF NOT EXISTS auth_session ( + id TEXT PRIMARY KEY, + expires_at TIMESTAMP NOT NULL, + token TEXT NOT NULL UNIQUE, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + ip_address TEXT, + user_agent TEXT, + user_id TEXT NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE + ) + `); + + await db.execute(sql` + CREATE INDEX IF NOT EXISTS auth_session_userId_idx ON auth_session(user_id) + `); + + await db.execute(sql` + CREATE TABLE IF NOT EXISTS auth_account ( + id TEXT PRIMARY KEY, + account_id TEXT NOT NULL, + provider_id TEXT NOT NULL, + user_id TEXT NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, + access_token TEXT, + refresh_token TEXT, + id_token TEXT, + access_token_expires_at TIMESTAMP, + refresh_token_expires_at TIMESTAMP, + scope TEXT, + password TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() + ) + `); + + await db.execute(sql` + CREATE INDEX IF NOT EXISTS auth_account_userId_idx ON auth_account(user_id) + `); + + await db.execute(sql` + CREATE TABLE IF NOT EXISTS auth_verification ( + id TEXT PRIMARY KEY, + identifier TEXT NOT NULL, + value TEXT NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() + ) + `); + + await db.execute(sql` + CREATE INDEX IF NOT EXISTS auth_verification_identifier_idx ON auth_verification(identifier) + `); + + try { + await db.execute(sql`ALTER TABLE auth_user ADD COLUMN IF NOT EXISTS slack_id TEXT`); + } catch {} + + // --- API Key tables --- + + await db.execute(sql` + CREATE TABLE IF NOT EXISTS api_keys ( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + name TEXT NOT NULL, + created_by_slack_id TEXT REFERENCES auth_user(id), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + revoked_by_slack_id TEXT REFERENCES auth_user(id), + key_prefix TEXT NOT NULL UNIQUE, + secret_hash TEXT NOT NULL, + last_used_at TIMESTAMP + ) + `); + + await db.execute(sql` + CREATE TABLE IF NOT EXISTS api_key_channels ( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + key_id INTEGER REFERENCES api_keys(id), + channel_id TEXT REFERENCES channels(id) + ) + `); + await client.end(); } diff --git a/src/db/schema.ts b/src/db/schema.ts index 00067c2..d579767 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,4 +1,5 @@ -import { pgTable, text, integer, jsonb, uniqueIndex, index } from "drizzle-orm/pg-core"; +import { pgTable, text, integer, jsonb, boolean, timestamp, uniqueIndex, index } from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; export const channels = pgTable("channels", { id: text("id").primaryKey(), @@ -12,6 +13,7 @@ export const channels = pgTable("channels", { trackReplies: text("track_replies").notNull().default("public"), metadataSchema: text("metadata_schema").notNull().default(""), createdAt: text("created_at").notNull().default("now()"), + accessPermUsers: text("access_perm_users").notNull().default("*"), }); export const messages = pgTable( @@ -25,7 +27,7 @@ export const messages = pgTable( text: text("text").notNull().default(""), threadTs: text("thread_ts"), timestamp: text("timestamp").notNull(), - metadata: jsonb("metadata").notNull().default("{}"), + metadata: text("metadata").notNull().default("{}"), }, (t) => ({ uniqueMsg: uniqueIndex("uq_messages_channel_ts").on(t.channelId, t.slackTs), @@ -50,6 +52,7 @@ export const subscriptions = pgTable( export const botActions = pgTable( "bot_actions", { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), type: text("type").notNull(), sourceChannelId: text("source_channel_id").notNull(), @@ -62,3 +65,101 @@ export const botActions = pgTable( sourceIdx: index("idx_bot_actions_source").on(t.sourceChannelId, t.sourceMessageTs), }), ); + +export const apiKeys = pgTable( + "api_keys", + { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + name: text("name").notNull(), + owner: text("created_by_slack_id").references(() => authUser.id), + createdAt: timestamp("created_at").defaultNow().notNull(), + revokedBy: text("revoked_by_slack_id").references(() => authUser.id), + keyPrefix: text("key_prefix").notNull().unique(), + secretHash: text("secret_hash").notNull(), + lastUsedAt: timestamp("last_used_at"), + // expiresAt: timestamp("expires_at", { mode: "date" }), + } +) + +export const apiKeyChannels = pgTable( + "api_key_channels", + { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + keyId: integer("key_id").references(() => apiKeys.id), + channelId: text("channel_id").references(() => channels.id) + } +) + +// --- Better Auth tables --- + +export const authUser = pgTable("auth_user", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").default(false).notNull(), + image: text("image"), + slackId: text("slack_id"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(), +}); + +export const authSession = pgTable( + "auth_session", + { + id: text("id").primaryKey(), + expiresAt: timestamp("expires_at").notNull(), + token: text("token").notNull().unique(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").$onUpdate(() => new Date()).notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id").notNull().references(() => authUser.id, { onDelete: "cascade" }), + }, + (t) => [index("auth_session_userId_idx").on(t.userId)], +); + +export const authAccount = pgTable( + "auth_account", + { + id: text("id").primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id").notNull().references(() => authUser.id, { onDelete: "cascade" }), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").$onUpdate(() => new Date()).notNull(), + }, + (t) => [index("auth_account_userId_idx").on(t.userId)], +); + +export const authVerification = pgTable( + "auth_verification", + { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().$onUpdate(() => new Date()).notNull(), + }, + (t) => [index("auth_verification_identifier_idx").on(t.identifier)], +); + +export const authUserRelations = relations(authUser, ({ many }) => ({ + sessions: many(authSession), + accounts: many(authAccount), +})); + +export const authSessionRelations = relations(authSession, ({ one }) => ({ + user: one(authUser, { fields: [authSession.userId], references: [authUser.id] }), +})); + +export const authAccountRelations = relations(authAccount, ({ one }) => ({ + user: one(authUser, { fields: [authAccount.userId], references: [authUser.id] }), +})); diff --git a/src/env.d.ts b/src/env.d.ts index 422eefb..7a69224 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -9,9 +9,14 @@ interface Env { HACK_CLUB_CDN_KEY: string; SLACK_CLIENT_ID: string; + // Hyperdrive binding + HYPERDRIVE_BINDING: { connectionString: string }; + // Secrets SLACK_BOT_TOKEN: string; SLACK_SIGNING_SECRET: string; DATABASE_URL: string; API_PASSWORD: string; + HACKCLUB_CLIENT_ID: string; + HACKCLUB_CLIENT_SECRET: string; } diff --git a/src/lib/api-keys.ts b/src/lib/api-keys.ts new file mode 100644 index 0000000..1d1725a --- /dev/null +++ b/src/lib/api-keys.ts @@ -0,0 +1,123 @@ +import { timingSafeEqual } from "node:crypto"; +import { eq } from "drizzle-orm"; +import { getDb } from "../db"; +import * as schema from "../db/schema"; +import { ORPCError } from "@orpc/server"; + +const KEY_PREFIX = "ind_"; +const KEY_PREFIX_LENGTH = 12; + +export function generateApiKey(): { fullKey: string; keyPrefix: string; secretHash: Promise } { + const array = new Uint8Array(24); + crypto.getRandomValues(array); + const secret = Array.from(array, (b) => b.toString(36).padStart(2, "0")).join(""); + const fullKey = KEY_PREFIX + secret; + const keyPrefix = secret.slice(0, KEY_PREFIX_LENGTH); + const secretHash = hashSecret(fullKey); + return { fullKey, keyPrefix, secretHash }; +} + +export async function hashSecret(secret: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(secret); + const hash = await crypto.subtle.digest("SHA-256", data); + return Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, "0")).join(""); +} + +export async function verifySecret( + secret: string, + storedHash: string, +): Promise { + const computedHash = await hashSecret(secret); + const a = Buffer.from(computedHash, "utf-8"); + const b = Buffer.from(storedHash, "utf-8"); + if (a.length !== b.length) return false; + return timingSafeEqual(a, b); +} + +export interface ApiKeyIdentity { + id: number; + keyPrefix: string; + name: string; + owner: string | null; + channelIds: string[]; +} + +export async function createApiKeyContext({ + req, + databaseUrl, +}: { + req: Request; + databaseUrl: string; +}): Promise { + const authHeader = req.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + throw new ORPCError("UNAUTHORIZED", { + message: "Invalid API key", + }); + } + + const token = authHeader.slice(7); + if (!token.startsWith(KEY_PREFIX)) { + throw new ORPCError("UNAUTHORIZED", { + message: "Invalid API key format", + }); + } + + const rawKey = token.slice(KEY_PREFIX.length); + if (rawKey.length < KEY_PREFIX_LENGTH) { + throw new ORPCError("UNAUTHORIZED", { + message: "Invalid API key", + }); + } + + const keyPrefix = rawKey.slice(0, KEY_PREFIX_LENGTH); + + const db = getDb(databaseUrl); + + const rows = await db + .select({ + id: schema.apiKeys.id, + keyPrefix: schema.apiKeys.keyPrefix, + name: schema.apiKeys.name, + owner: schema.apiKeys.owner, + secretHash: schema.apiKeys.secretHash, + lastUsedAt: schema.apiKeys.lastUsedAt, + channelId: schema.apiKeyChannels.channelId, + }) + .from(schema.apiKeys) + .leftJoin(schema.apiKeyChannels, eq(schema.apiKeys.id, schema.apiKeyChannels.keyId)) + .where(eq(schema.apiKeys.keyPrefix, keyPrefix)); + + if (rows.length === 0) { + throw new ORPCError("UNAUTHORIZED", { + message: "Invalid API key", + }); + } + + const keyRow = rows[0]!; + + const valid = await verifySecret(token, keyRow.secretHash); + if (!valid) { + throw new ORPCError("UNAUTHORIZED", { + message: "Invalid API key", + }); + } + + const channelIds = rows + .filter((r) => r.channelId) + .map((r) => r.channelId!); + + await db + .update(schema.apiKeys) + .set({ lastUsedAt: new Date() }) + .where(eq(schema.apiKeys.id, keyRow.id)); + + return { + id: keyRow.id, + keyPrefix: keyRow.keyPrefix, + name: keyRow.name, + owner: keyRow.owner, + channelIds, + }; +} diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..4beb8b8 --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,7 @@ +import { createAuthClient } from "better-auth/react"; +import { genericOAuthClient } from "better-auth/client/plugins"; + +export const authClient = createAuthClient({ + baseURL: window.location.origin, + plugins: [genericOAuthClient()], +}); diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..4404211 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,102 @@ +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { genericOAuth } from "better-auth/plugins"; +import { getDb } from "../db"; +import * as schema from "../db/schema"; +import { eq } from "drizzle-orm"; + +export function getAuth(databaseUrl: string, env?: Record) { + const db = getDb(databaseUrl); + return betterAuth({ + database: drizzleAdapter(db, { + provider: "pg", + schema: { + user: schema.authUser, + session: schema.authSession, + account: schema.authAccount, + verification: schema.authVerification, + }, + }), + user: { + additionalFields: { + slackId: { + type: "string", + required: false, + }, + }, + }, + plugins: [ + genericOAuth({ + config: [ + { + providerId: "hackclub", + discoveryUrl: "https://auth.hackclub.com/.well-known/openid-configuration", + clientId: env?.HACKCLUB_CLIENT_ID || "", + clientSecret: env?.HACKCLUB_CLIENT_SECRET || "", + scopes: ["openid", "profile", "email", "slack_id"], + }, + ], + }), + ], + databaseHooks: { + user: { + create: { + after: async (user) => { + await setSlackIdFromAccount(databaseUrl, user.id); + }, + }, + update: { + after: async (user) => { + await setSlackIdFromAccount(databaseUrl, user.id); + }, + }, + }, + }, + baseURL: env?.BASE_URL || "http://localhost:8787", + trustedOrigins: [ + "http://localhost:8787", + "http://indigest.matmanna.dev", + "https://indigest.matmanna.dev", + ], + }); +} + +/** + * After user create/update, read the id_token from auth_account, decode it, + * and set slack_id on auth_user if present. + */ +async function setSlackIdFromAccount(databaseUrl: string, userId: string): Promise { + try { + const db = getDb(databaseUrl); + const accounts = await db + .select({ idToken: schema.authAccount.idToken }) + .from(schema.authAccount) + .where(eq(schema.authAccount.userId, userId)); + const idToken = accounts[0]?.idToken; + if (!idToken) { + console.log(`setSlackId: no id_token for user ${userId}`); + return; + } + + const parts = idToken.split("."); + if (parts.length !== 3 || !parts[1]) return; + const payload = JSON.parse(atob(parts[1])); + if (!payload.slack_id) { + console.log(`setSlackId: no slack_id in token for user ${userId}`); + return; + } + + await db + .update(schema.authUser) + .set({ slackId: payload.slack_id }) + .where(eq(schema.authUser.id, userId)); + console.log(`setSlackId: set slack_id=${payload.slack_id} for user ${userId}`); + } catch (err: any) { + console.error("setSlackIdFromAccount failed:", err.message); + } +} + +// Default export for `npx auth generate` — uses DATABASE_URL env var +const databaseUrl = process.env.DATABASE_URL || ""; +export const auth = databaseUrl ? getAuth(databaseUrl, process.env as Record) : (null as any); +export default auth; diff --git a/src/store/neon.ts b/src/store/neon.ts index 7457b20..f62022a 100644 --- a/src/store/neon.ts +++ b/src/store/neon.ts @@ -24,6 +24,7 @@ export class NeonStore implements Store { webhookUrl: r.webhook_url, autoApproveUsers: r.auto_approve_users ? r.auto_approve_users.split(",").filter(Boolean) : [], approvedPosters: r.approved_posters ? r.approved_posters.split(",").filter(Boolean) : [], + accessPermUsers: r.access_perm_users ? r.access_perm_users.split(",").filter(Boolean) : ["*"], trackReplies: Boolean(r.track_replies), metadataSchema: r.metadata_schema, createdAt: r.created_at, @@ -31,9 +32,9 @@ export class NeonStore implements Store { } async upsertChannel(ch: StoreChannel): Promise { - await this.sql`INSERT INTO channels (id, name, team_id, enabled, link_mode, webhook_url, auto_approve_users, approved_posters, track_replies, metadata_schema, created_at) - VALUES (${ch.id}, ${ch.name}, ${ch.teamId}, ${ch.enabled ? 1 : 0}, ${ch.linkMode ? 1 : 0}, ${ch.webhookUrl}, ${ch.autoApproveUsers.join(",")}, ${ch.approvedPosters.join(",")}, ${ch.trackReplies ? 1 : 0}, ${ch.metadataSchema}, ${new Date().toISOString()}) - ON CONFLICT (id) DO UPDATE SET name = ${ch.name}, team_id = ${ch.teamId}, enabled = ${ch.enabled ? 1 : 0}, link_mode = ${ch.linkMode ? 1 : 0}, webhook_url = ${ch.webhookUrl}, auto_approve_users = ${ch.autoApproveUsers.join(",")}, approved_posters = ${ch.approvedPosters.join(",")}, track_replies = ${ch.trackReplies ? 1 : 0}, metadata_schema = ${ch.metadataSchema}`; + await this.sql`INSERT INTO channels (id, name, team_id, enabled, link_mode, webhook_url, auto_approve_users, approved_posters, access_perm_users, track_replies, metadata_schema, created_at) + VALUES (${ch.id}, ${ch.name}, ${ch.teamId}, ${ch.enabled ? 1 : 0}, ${ch.linkMode ? 1 : 0}, ${ch.webhookUrl}, ${ch.autoApproveUsers.join(",")}, ${ch.approvedPosters.join(",")}, ${ch.accessPermUsers.join(",")}, ${ch.trackReplies ? 1 : 0}, ${ch.metadataSchema}, ${new Date().toISOString()}) + ON CONFLICT (id) DO UPDATE SET name = ${ch.name}, team_id = ${ch.teamId}, enabled = ${ch.enabled ? 1 : 0}, link_mode = ${ch.linkMode ? 1 : 0}, webhook_url = ${ch.webhookUrl}, auto_approve_users = ${ch.autoApproveUsers.join(",")}, approved_posters = ${ch.approvedPosters.join(",")}, access_perm_users = ${ch.accessPermUsers.join(",")}, track_replies = ${ch.trackReplies ? 1 : 0}, metadata_schema = ${ch.metadataSchema}`; } async listChannels(): Promise { @@ -46,6 +47,7 @@ export class NeonStore implements Store { webhookUrl: r.webhook_url, autoApproveUsers: r.auto_approve_users ? r.auto_approve_users.split(",").filter(Boolean) : [], approvedPosters: r.approved_posters ? r.approved_posters.split(",").filter(Boolean) : [], + accessPermUsers: r.access_perm_users ? r.access_perm_users.split(",").filter(Boolean) : ["*"], trackReplies: Boolean(r.track_replies), metadataSchema: r.metadata_schema, createdAt: r.created_at, @@ -62,6 +64,7 @@ export class NeonStore implements Store { webhookUrl: r.webhook_url, autoApproveUsers: r.auto_approve_users ? r.auto_approve_users.split(",").filter(Boolean) : [], approvedPosters: r.approved_posters ? r.approved_posters.split(",").filter(Boolean) : [], + accessPermUsers: r.access_perm_users ? r.access_perm_users.split(",").filter(Boolean) : ["*"], trackReplies: Boolean(r.track_replies), metadataSchema: r.metadata_schema, createdAt: r.created_at, @@ -155,4 +158,48 @@ export class NeonStore implements Store { createdAt: r.created_at, })); } + + private rowToChannel(r: any): StoreChannel { + return { + id: r.id, + name: r.name, + teamId: r.team_id, + enabled: Boolean(r.enabled), + linkMode: Boolean(r.link_mode), + webhookUrl: r.webhook_url, + autoApproveUsers: r.auto_approve_users ? r.auto_approve_users.split(",").filter(Boolean) : [], + approvedPosters: r.approved_posters ? r.approved_posters.split(",").filter(Boolean) : [], + accessPermUsers: r.access_perm_users ? r.access_perm_users.split(",").filter(Boolean) : ["*"], + trackReplies: Boolean(r.track_replies), + metadataSchema: r.metadata_schema, + createdAt: r.created_at, + }; + } + + async getGraphData(): Promise<{ channels: StoreChannel[]; subscriptions: StoreSubscription[]; subscriberChannels: StoreChannel[] }> { + const [channelRows, subscriptionRows] = await Promise.all([ + this.sql`SELECT * FROM channels`, + this.sql`SELECT * FROM subscriptions`, + ]); + + const channels = rows(channelRows).map((r) => this.rowToChannel(r)); + + const channelIds = new Set(channels.map((c) => c.id)); + const subscriberChannelIds = [...new Set(rows(subscriptionRows).map((r) => r.subscriber_channel_id).filter((id: string) => !channelIds.has(id)))]; + + let subscriberChannels: StoreChannel[] = []; + if (subscriberChannelIds.length > 0) { + const subRows = await this.sql`SELECT * FROM channels WHERE id = ANY(${subscriberChannelIds})`; + subscriberChannels = rows(subRows).map((r) => this.rowToChannel(r)); + } + + const subscriptions = rows(subscriptionRows).map((r) => ({ + id: r.id, + subscriberChannelId: r.subscriber_channel_id, + sourceChannelId: r.source_channel_id, + createdAt: r.created_at, + })); + + return { channels, subscriptions, subscriberChannels }; + } } diff --git a/src/store/pg.test.ts b/src/store/pg.test.ts index 9e51984..e622eab 100644 --- a/src/store/pg.test.ts +++ b/src/store/pg.test.ts @@ -30,7 +30,7 @@ afterAll(() => { describe("PostgresStore", () => { it("should insert and retrieve a channel", async () => { - await store.upsertChannel({ id: "C001", name: "general", teamId: "T001", enabled: false, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], trackReplies: false, metadataSchema: "", createdAt: "" }); + await store.upsertChannel({ id: "C001", name: "general", teamId: "T001", enabled: false, linkMode: false, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], accessPermUsers: ["*"], trackReplies: false, metadataSchema: "", createdAt: "" }); const ch = await store.getChannel("C001"); expect(ch).not.toBeNull(); expect(ch!.name).toBe("general"); @@ -38,15 +38,15 @@ describe("PostgresStore", () => { }); it("should enable a channel via upsert", async () => { - await store.upsertChannel({ id: "C001", name: "general", teamId: "T001", enabled: true, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], trackReplies: false, metadataSchema: "", createdAt: "" }); + await store.upsertChannel({ id: "C001", name: "general", teamId: "T001", enabled: true, linkMode: false, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], accessPermUsers: ["*"], trackReplies: false, metadataSchema: "", createdAt: "" }); const ch = await store.getChannel("C001"); expect(ch!.enabled).toBe(true); }); it("should update channel fields on upsert", async () => { await store.upsertChannel({ - id: "C001", name: "updated-name", teamId: "T001", enabled: true, - webhookUrl: "https://hook.example.com", autoApproveUsers: ["U1", "U2"], approvedPosters: [], trackReplies: false, metadataSchema: "", createdAt: "", + id: "C001", name: "updated-name", teamId: "T001", enabled: true, linkMode: false, + webhookUrl: "https://hook.example.com", autoApproveUsers: ["U1", "U2"], approvedPosters: [], accessPermUsers: ["*"], trackReplies: false, metadataSchema: "", createdAt: "", }); const ch = await store.getChannel("C001"); expect(ch!.name).toBe("updated-name"); @@ -55,8 +55,8 @@ describe("PostgresStore", () => { }); it("should list only enabled channels", async () => { - await store.upsertChannel({ id: "C002", name: "random", teamId: "T001", enabled: false, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], trackReplies: false, metadataSchema: "", createdAt: "" }); - await store.upsertChannel({ id: "C003", name: "dev", teamId: "T001", enabled: true, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], trackReplies: false, metadataSchema: "", createdAt: "" }); + await store.upsertChannel({ id: "C002", name: "random", teamId: "T001", enabled: false, linkMode: false, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], accessPermUsers: ["*"], trackReplies: false, metadataSchema: "", createdAt: "" }); + await store.upsertChannel({ id: "C003", name: "dev", teamId: "T001", enabled: true, linkMode: false, webhookUrl: "", autoApproveUsers: [], approvedPosters: [], accessPermUsers: ["*"], trackReplies: false, metadataSchema: "", createdAt: "" }); const enabled = await store.listEnabledChannels(); expect(enabled.length).toBe(2); expect(enabled.map((c) => c.id).sort()).toEqual(["C001", "C003"]); @@ -103,7 +103,7 @@ describe("PostgresStore", () => { }); it("should persist webhookUrl and autoApproveUsers on channel", async () => { - await store.upsertChannel({ id: "C010", name: "webhook-test", teamId: "T001", enabled: true, webhookUrl: "https://hooks.example.com/feed", autoApproveUsers: ["U100", "U200"], approvedPosters: [], trackReplies: false, metadataSchema: "", createdAt: "" }); + await store.upsertChannel({ id: "C010", name: "webhook-test", teamId: "T001", enabled: true, linkMode: false, webhookUrl: "https://hooks.example.com/feed", autoApproveUsers: ["U100", "U200"], approvedPosters: [], accessPermUsers: ["*"], trackReplies: false, metadataSchema: "", createdAt: "" }); const ch = await store.getChannel("C010"); expect(ch!.webhookUrl).toBe("https://hooks.example.com/feed"); expect(ch!.autoApproveUsers).toEqual(["U100", "U200"]); diff --git a/src/store/pg.ts b/src/store/pg.ts index 2e94f3b..7728725 100644 --- a/src/store/pg.ts +++ b/src/store/pg.ts @@ -1,6 +1,6 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; -import { eq, sql, and } from "drizzle-orm"; +import { eq, sql, and, inArray } from "drizzle-orm"; import * as schema from "../db/schema"; import type { Store, StoreChannel, StoreMessage, StoreSubscription, StoreBotAction } from "./store"; @@ -17,7 +17,7 @@ export class PostgresStore implements Store { const rows = await this.db.select().from(schema.channels).where(eq(schema.channels.id, id)).limit(1); if (rows.length === 0) return null; const r = rows[0]!; - return { id: r.id, name: r.name, teamId: r.teamId, enabled: Boolean(r.enabled), linkMode: Boolean(r.linkMode), webhookUrl: r.webhookUrl, autoApproveUsers: r.autoApproveUsers ? r.autoApproveUsers.split(",").filter(Boolean) : [], approvedPosters: r.approvedPosters ? r.approvedPosters.split(",").filter(Boolean) : [], trackReplies: Boolean(r.trackReplies), metadataSchema: r.metadataSchema, createdAt: r.createdAt }; + return { accessPermUsers: r.accessPermUsers ? r.accessPermUsers.split(",").filter(Boolean) : ["*"], id: r.id, name: r.name, teamId: r.teamId, enabled: Boolean(r.enabled), linkMode: Boolean(r.linkMode), webhookUrl: r.webhookUrl, autoApproveUsers: r.autoApproveUsers ? r.autoApproveUsers.split(",").filter(Boolean) : [], approvedPosters: r.approvedPosters ? r.approvedPosters.split(",").filter(Boolean) : [], trackReplies: Boolean(r.trackReplies), metadataSchema: r.metadataSchema, createdAt: r.createdAt }; } async upsertChannel(ch: StoreChannel): Promise { @@ -32,6 +32,7 @@ export class PostgresStore implements Store { webhookUrl: ch.webhookUrl, autoApproveUsers: ch.autoApproveUsers.join(","), approvedPosters: ch.approvedPosters.join(","), + accessPermUsers: ch.accessPermUsers.join(","), trackReplies: ch.trackReplies ? 1 : 0, metadataSchema: ch.metadataSchema, createdAt: sql`COALESCE((SELECT created_at FROM channels WHERE id = ${ch.id}), now()::text)`, @@ -46,6 +47,7 @@ export class PostgresStore implements Store { webhookUrl: ch.webhookUrl, autoApproveUsers: ch.autoApproveUsers.join(","), approvedPosters: ch.approvedPosters.join(","), + accessPermUsers: ch.accessPermUsers.join(","), trackReplies: ch.trackReplies ? 1 : 0, metadataSchema: ch.metadataSchema, }, @@ -62,6 +64,7 @@ export class PostgresStore implements Store { linkMode: Boolean(r.linkMode), webhookUrl: r.webhookUrl, autoApproveUsers: r.autoApproveUsers ? r.autoApproveUsers.split(",").filter(Boolean) : [], + accessPermUsers: r.accessPermUsers ? r.accessPermUsers.split(",").filter(Boolean) : [], approvedPosters: r.approvedPosters ? r.approvedPosters.split(",").filter(Boolean) : [], trackReplies: Boolean(r.trackReplies), metadataSchema: r.metadataSchema, @@ -71,7 +74,7 @@ export class PostgresStore implements Store { async listEnabledChannels(): Promise { const rows = await this.db.select().from(schema.channels).where(eq(schema.channels.enabled, 1)); - return rows.map((r) => ({ id: r.id, name: r.name, teamId: r.teamId, enabled: true, linkMode: Boolean(r.linkMode), webhookUrl: r.webhookUrl, autoApproveUsers: r.autoApproveUsers ? r.autoApproveUsers.split(",").filter(Boolean) : [], approvedPosters: r.approvedPosters ? r.approvedPosters.split(",").filter(Boolean) : [], trackReplies: Boolean(r.trackReplies), metadataSchema: r.metadataSchema, createdAt: r.createdAt })); + return rows.map((r) => ({ id: r.id, name: r.name, teamId: r.teamId, enabled: true, linkMode: Boolean(r.linkMode), webhookUrl: r.webhookUrl, autoApproveUsers: r.autoApproveUsers ? r.autoApproveUsers.split(",").filter(Boolean) : [], approvedPosters: r.approvedPosters ? r.approvedPosters.split(",").filter(Boolean) : [], accessPermUsers: r.accessPermUsers ? r.accessPermUsers.split(",").filter(Boolean) : [], trackReplies: Boolean(r.trackReplies), metadataSchema: r.metadataSchema, createdAt: r.createdAt })); } async upsertMessage(msg: StoreMessage): Promise { @@ -220,4 +223,49 @@ export class PostgresStore implements Store { createdAt: r.createdAt, })); } + + private rowToChannel(r: any): StoreChannel { + return { + id: r.id, + name: r.name, + teamId: r.teamId, + enabled: Boolean(r.enabled), + linkMode: Boolean(r.linkMode), + webhookUrl: r.webhookUrl, + autoApproveUsers: r.autoApproveUsers ? r.autoApproveUsers.split(",").filter(Boolean) : [], + approvedPosters: r.approvedPosters ? r.approvedPosters.split(",").filter(Boolean) : [], + accessPermUsers: r.accessPermUsers ? r.accessPermUsers.split(",").filter(Boolean) : [], + trackReplies: Boolean(r.trackReplies), + metadataSchema: r.metadataSchema, + createdAt: r.createdAt, + }; + } + + async getGraphData(): Promise<{ channels: StoreChannel[]; subscriptions: StoreSubscription[]; subscriberChannels: StoreChannel[] }> { + const [channelRows, subscriptionRows] = await Promise.all([ + this.db.select().from(schema.channels), + this.db.select().from(schema.subscriptions), + ]); + + const channels = channelRows.map((r) => this.rowToChannel(r)); + + // Find subscriber channels that aren't already in the channels list + const channelIds = new Set(channels.map((c) => c.id)); + const subscriberChannelIds = [...new Set(subscriptionRows.map((r) => r.subscriberChannelId).filter((id) => !channelIds.has(id)))]; + + let subscriberChannels: StoreChannel[] = []; + if (subscriberChannelIds.length > 0) { + const subRows = await this.db.select().from(schema.channels).where(inArray(schema.channels.id, subscriberChannelIds)); + subscriberChannels = subRows.map((r) => this.rowToChannel(r)); + } + + const subscriptions = subscriptionRows.map((r) => ({ + id: r.id, + subscriberChannelId: r.subscriberChannelId, + sourceChannelId: r.sourceChannelId, + createdAt: r.createdAt, + })); + + return { channels, subscriptions, subscriberChannels }; + } } diff --git a/src/store/store.ts b/src/store/store.ts index b9be825..6720c13 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -7,6 +7,7 @@ export interface StoreChannel { webhookUrl: string; autoApproveUsers: string[]; approvedPosters: string[]; + accessPermUsers: string[]; trackReplies: boolean; metadataSchema: string; createdAt: string; @@ -56,5 +57,6 @@ export interface Store { getRecentMessages(channelId: string, since: Date): Promise; addBotAction(action: StoreBotAction): Promise; getBotActionsBySource(sourceChannelId: string, sourceMessageTs: string): Promise; + getGraphData(): Promise<{ channels: StoreChannel[]; subscriptions: StoreSubscription[]; subscriberChannels: StoreChannel[] }>; close(): void; } diff --git a/src/web/app.tsx b/src/web/app.tsx index 74d4511..76228e9 100644 --- a/src/web/app.tsx +++ b/src/web/app.tsx @@ -13,6 +13,7 @@ import { } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import "./lockdown"; +import { renderNavbar } from "./navbar" const TEAM_ID = "T024F7C8B"; const slackLink = (channelId: string) => @@ -42,15 +43,7 @@ const highlightJson = (json: string): string => } ); -document.getElementById("navbar")!.innerHTML = ``; +renderNavbar("map") interface GraphNode { id: string; type: string; label: string; data: Record; } interface GraphEdge { id: string; source: string; target: string; label?: string; animated: boolean; } diff --git a/src/web/docs.tsx b/src/web/docs.tsx index 66948f9..3797fd6 100644 --- a/src/web/docs.tsx +++ b/src/web/docs.tsx @@ -1,14 +1,7 @@ import "./lockdown"; +import { renderNavbar } from "./navbar"; -const navEl = document.getElementById("navbar")!; -navEl.innerHTML = ``; +renderNavbar("docs"); const script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/@scalar/api-reference"; diff --git a/src/web/keys.tsx b/src/web/keys.tsx new file mode 100644 index 0000000..8f21577 --- /dev/null +++ b/src/web/keys.tsx @@ -0,0 +1,118 @@ +import { useState, useEffect } from "react"; +import { createRoot } from "react-dom/client"; +import { renderNavbar } from "./navbar"; +import { authClient } from "../lib/auth-client"; + +renderNavbar("usage"); + +function KeysList() { + const { data: session, isPending } = authClient.useSession(); + const [channels, setChannels] = useState([]); + const [keys, setKeys] = useState([]); + const [keysPending, setKeysPending] = useState(true); + const [channelsPending, setChannelsPending] = useState(true); + const [error, setError] = useState(null); + const [createdKey, setCreatedKey] = useState(null); + + useEffect(() => { + if (session?.user?.slackId) { + fetch(`/api/channels/by-user/${session.user.id}`) + .then((r) => { + if (!r.ok) throw new Error(`HTTP ${r.status}`); + return r.json(); + }) + .then((data) => {setChannels(Array.isArray(data) ? data : []);setChannelsPending(false)}) + .catch((e) => setError(String(e))); + fetch('api/api-keys').then((r) => r.json()).then((data) => {setKeys(Array.isArray(data) ? data : []);setKeysPending(false)}).catch((e) => setError(String(e))); + } + }, [session?.user?.slackId]); + + if (isPending) return

loading auth...

; + if (!session?.user?.slackId) { + return ( +
+

your api keys

+

session: {JSON.stringify(session, null, 2)}

+
+ ); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setCreatedKey(null); + + const formData = new FormData(e.currentTarget); + const name = formData.get("name") as string; + const channelIds = formData.getAll("channels") as string[]; + + try { + const res = await fetch("/api/api-keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, channelIds }), + }); + const data = await res.json(); + if (data.fullKey) { + setCreatedKey(data.fullKey); + } else { + setError(data.message || JSON.stringify(data)); + } + } catch (e: any) { + setError(String(e)); + } + } + + return ( +
+ {error &&

Error: {error}

} + {createdKey && ( +
+

Key created!

+

Copy this key now — it won't be shown again:

+ {createdKey} +
+ )} +
+

make a new api key

+ + + + + + + + + +
+ +
+

your keys

+ {keysPending ? "loading keys..." : null} + {keys.map((k) => ( +
+{k.name} ind_{k.keyPrefix}... {k.revokedBy && `Revoked by ${revokedBy}`} +
+))} +
+
+ ); +} + +createRoot(document.getElementById("app")!).render( +
+

indigestion api keys

+

for now, all keys are read-only

+ +
+); diff --git a/src/web/lockdown.ts b/src/web/lockdown.ts index 62fb149..954a596 100644 --- a/src/web/lockdown.ts +++ b/src/web/lockdown.ts @@ -25,11 +25,15 @@ function renderLockdownBanner(users: LockdownUser[]) { if (index < users.length - 1) banner.appendChild(document.createTextNode(", ")); }); - banner.appendChild(document.createTextNode(") to configure the bot.")); + banner.appendChild(document.createTextNode(") to configure the bot")); document.body.insertBefore(banner, document.body.firstChild); } fetch("/site-config") .then((response): Promise<{ lockdownUsers: LockdownUser[] }> | null => response.ok ? response.json() : null) - .then((config) => renderLockdownBanner(config?.lockdownUsers || [])) + .then((config) => { + const users = config?.lockdownUsers || []; + if (users.length === 0 || !document.body) return; + renderLockdownBanner(users); + }) .catch(() => {}); diff --git a/src/web/navbar.tsx b/src/web/navbar.tsx index 1acd531..2e1eebd 100644 --- a/src/web/navbar.tsx +++ b/src/web/navbar.tsx @@ -1,19 +1,68 @@ -import { render } from "preact"; +import { useState, useEffect } from "react"; +import { createRoot } from "react-dom/client"; +import { authClient } from "../lib/auth-client"; + +function Navbar({ active }: { active: string }) { + const { data: session, isPending } = authClient.useSession(); + const [stars, setStars] = useState(null); + + useEffect(() => { + const cached = localStorage.getItem("gh_stars"); + const cachedAt = localStorage.getItem("gh_stars_at"); + if (cached && cachedAt && Date.now() - Number(cachedAt) < 10 * 60 * 1000) { + setStars(Number(cached)); + return; + } + fetch("/site-config") + .then((r) => r.json()) + .then((d: any) => { + if (d.githubStars != null) { + setStars(d.githubStars); + localStorage.setItem("gh_stars", String(d.githubStars)); + localStorage.setItem("gh_stars_at", String(Date.now())); + } + }) + .catch(() => {}); + }, []); -export function Navbar({ active }: { active: string }) { return ( ); } export function renderNavbar(active: string) { - render(, document.getElementById("navbar")!); + const el = document.getElementById("navbar"); + if (el) createRoot(el).render(); } diff --git a/src/web/style.css b/src/web/style.css index 5eed330..9f9ae1f 100644 --- a/src/web/style.css +++ b/src/web/style.css @@ -69,6 +69,7 @@ body { gap: 1.5rem; padding: 0.5rem 1rem; background: var(--surface); + flex-wrap: wrap; border-bottom: 1px solid var(--border); position: sticky; top: 0; @@ -80,9 +81,10 @@ body { color: var(--text); text-decoration: none; } -.navbar-links { +.navbar-links-main { display: flex; gap: 0.75rem; + flex-grow: 1; } .navbar-links a { color: var(--text-muted); @@ -312,3 +314,91 @@ body { from { stroke-dashoffset: 10; } to { stroke-dashoffset: 0; } } + +.button { + display: inline-flex; + align-items: center; + margin-left: 8px; + margin-right: 8px; + justify-content: center; + flex-shrink: 0; + gap: 0.375rem; + height: 1.75rem; + padding: 0 0.5rem; + cursor: pointer; + white-space: nowrap; + background: white; + color: black; + font-size: 0.75rem; + font-weight: 500; + line-height: 1; + border: none; + outline: none; + box-shadow: 2; + transition: all 0.2s ease; + } + + .red { + background-color: #983b3d; + color: white; + } + + .card, .input { + border: 1px solid; + display: flex; + flex-direction: column; + gap: 12px; + background-color: #070707; + color: white; +padding: .5rem; + } + + .card { + padding: 1rem; + } + + + .column { + display: flex; + flex-direction: column; + gap: 12px; + } + + .badge { + display: inline-flex; + align-items: center; + + padding: 0.25rem 0.5rem; /* py-1 px-2 */ + + border-radius: 0.375rem; /* rounded-md */ + + background-color: #1d2635; /* bg-yellow-400/10 */ + color: #99a1af; /* text-yellow-500 */ + + font-size: 0.75rem; /* text-xs */ + line-height: 1rem; + font-size: 0.8em; + font-weight: 500; /* font-medium */ + + /* inset-ring inset-ring-yellow-400/20 */ + box-shadow: inset 0 0 0 1px rgb(250 204 21 / 0.2); +} + +@keyframes rapid-zoom { + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.15); + } +} + +.rot-badge { + position: absolute; + rotate: -6deg; + translate: -70px 20px; + animation: rapid-zoom 1800ms ease-in-out infinite; + transform-origin: center; +} diff --git a/src/web/usage.tsx b/src/web/usage.tsx index 39c9df5..4caf63c 100644 --- a/src/web/usage.tsx +++ b/src/web/usage.tsx @@ -2,8 +2,11 @@ import Markdown from 'react-markdown' import {createRoot} from 'react-dom/client' import remarkGfm from 'remark-gfm' import './lockdown' +import { renderNavbar } from "./navbar" -const markdown = `# Slack Usage +renderNavbar("usage") + +const markdown = `# Slack Bot Usage Slash commands for managing indigest channels. Run these in Slack like: @@ -70,15 +73,6 @@ If you want to add an older message to the feed without waiting for the Yep!/No - \`/in status\` — Show channel status, feeds, and permissions.`; -document.getElementById("navbar")!.innerHTML = ``; - createRoot(document.getElementById("app")!).render(
{markdown} diff --git a/usage.html b/usage.html index 722de93..663543b 100644 --- a/usage.html +++ b/usage.html @@ -2,8 +2,11 @@ + + + - indigest — Slack Docs + indigest - slack bot usage diff --git a/vite.config.ts b/vite.config.ts index 04573b2..5232128 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,8 @@ export default defineConfig({ input: { main: "index.html", docs: "docs.html", - usage: "usage.html" + usage: "usage.html", + keys: "keys.html" }, }, }, diff --git a/wrangler.jsonc b/wrangler.jsonc index 60cfa4b..1697016 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,28 +1,51 @@ { - "$schema": "./node_modules/wrangler/config-schema.json", - "name": "indigest", - "main": "src/worker.ts", - "compatibility_date": "2025-07-21", - "compatibility_flags": ["nodejs_compat"], - "observability": { - "enabled": true - }, - "assets": { - "directory": "./dist", - "not_found_handling": "none", - "run_worker_first": [ - "/api/*", - "/feed/*", - "/events", - "/interactions", - "/slack", - "/slack/", - "/spec.json" - ] - }, - "vars": { - "API_USERNAME": "admin", - "LOCKDOWN_USERS": "U07VA44DNBA,U09Q8MLTE58,U07EB2Y76DP", - "SLACK_CLIENT_ID": "2210535565.11557816933424", - } + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "indigest", + "main": "src/worker.ts", + "compatibility_date": "2025-07-21", + "compatibility_flags": [ + "nodejs_compat" + ], + "observability": { + "enabled": true + }, + "routes": [ + { + "pattern": "indigest.matmanna.dev/*", + "zone_name": "matmanna.dev" + }, + { + "pattern": "*-indigest.matmanna.dev/*", + "zone_name": "matmanna.dev" + }, + { + "pattern": "indigest.matmanna.dev", + "zone_name": "matmanna.dev", + "custom_domain": true + } + ], + "assets": { + "directory": "./dist", + "not_found_handling": "none", + "run_worker_first": [ + "/api/*", + "/feed/*", + "/events", + "/interactions", + "/slack", + "/slack/", + "/spec.json" + ] + }, + "hyperdrive": [ + { + "binding": "HYPERDRIVE_BINDING", + "id": "af08c1f1e52543949fae23f29a7f939b" + } + ], + "vars": { + "API_USERNAME": "admin", + "LOCKDOWN_USERS": "U07VA44DNBA,U09Q8MLTE58,U07EB2Y76DP", + "SLACK_CLIENT_ID": "2210535565.11557816933424", + } } -- 2.51.2