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}
+
+ )}
+
+
+
+
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 (