From 476a068a8ee408f33f1d8de5dab282ab27976312 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Fri, 27 Mar 2026 23:29:28 +0800 Subject: [PATCH] feat: integrate better-auth for OIDC authentication on Cloudflare Workers Add better-auth (v1.5.6) to protect the admin API with session-based authentication backed by Cloudflare D1 via the Kysely adapter. - Mount /api/auth/* handler (sign-in, callback, sign-out, session) - Guard GET /api/freshman with session validation (Bearer Token + Cookie) - POST /api/freshman remains public for open recruitment form submissions - Add genericOAuth plugin wired to Logto as the OIDC provider (PKCE) - Add bearer plugin to support Authorization header token flow - Enable nodejs_compat compatibility flag required by better-auth - Add LOGTO_ISSUER / LOGTO_CALLBACK_URL env vars to wrangler.jsonc - Extend Env interface with BETTER_AUTH_SECRET, LOGTO_* secrets - Add D1 migration SQL for better-auth tables (user/session/account/verification) --- migrations/0001_better_auth.sql | 48 +++++ package.json | 2 + pnpm-lock.yaml | 339 ++++++++++++++++++++++++++++++++ src/auth.ts | 37 ++++ src/index.ts | 21 +- worker-configuration.d.ts | 5 + wrangler.jsonc | 6 +- 7 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 migrations/0001_better_auth.sql create mode 100644 src/auth.ts diff --git a/migrations/0001_better_auth.sql b/migrations/0001_better_auth.sql new file mode 100644 index 0000000..9c6c08b --- /dev/null +++ b/migrations/0001_better_auth.sql @@ -0,0 +1,48 @@ +-- better-auth required tables (SQLite / Cloudflare D1) +-- Generated for better-auth v1.5 with genericOAuth + bearer plugins + +CREATE TABLE IF NOT EXISTS `user` ( + `id` TEXT NOT NULL PRIMARY KEY, + `name` TEXT NOT NULL, + `email` TEXT NOT NULL UNIQUE, + `emailVerified` INTEGER NOT NULL DEFAULT 0, + `image` TEXT, + `createdAt` TEXT NOT NULL, + `updatedAt` TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS `session` ( + `id` TEXT NOT NULL PRIMARY KEY, + `userId` TEXT NOT NULL REFERENCES `user`(`id`), + `token` TEXT NOT NULL UNIQUE, + `expiresAt` TEXT NOT NULL, + `ipAddress` TEXT, + `userAgent` TEXT, + `createdAt` TEXT NOT NULL, + `updatedAt` TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS `account` ( + `id` TEXT NOT NULL PRIMARY KEY, + `userId` TEXT NOT NULL REFERENCES `user`(`id`), + `accountId` TEXT NOT NULL, + `providerId` TEXT NOT NULL, + `accessToken` TEXT, + `refreshToken` TEXT, + `idToken` TEXT, + `accessTokenExpiresAt` TEXT, + `refreshTokenExpiresAt` TEXT, + `scope` TEXT, + `password` TEXT, + `createdAt` TEXT NOT NULL, + `updatedAt` TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS `verification` ( + `id` TEXT NOT NULL PRIMARY KEY, + `identifier` TEXT NOT NULL, + `value` TEXT NOT NULL, + `expiresAt` TEXT NOT NULL, + `createdAt` TEXT, + `updatedAt` TEXT +); diff --git a/package.json b/package.json index 7c1f58b..6ef7181 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "check": "npx @biomejs/biome check --write ./src" }, "dependencies": { + "@better-auth/kysely-adapter": "^1.5.6", "@scalar/hono-api-reference": "^0.9.24", + "better-auth": "^1.5.6", "chanfana": "^2.8.3", "hono": "^4.10.4", "kysely": "^0.28.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63f3100..c34f773 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,15 @@ importers: .: dependencies: + '@better-auth/kysely-adapter': + specifier: ^1.5.6 + version: 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.8) '@scalar/hono-api-reference': specifier: ^0.9.24 version: 0.9.24(hono@4.10.4) + better-auth: + specifier: ^1.5.6 + version: 1.5.6(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1) chanfana: specifier: ^2.8.3 version: 2.8.3 @@ -50,6 +56,81 @@ packages: peerDependencies: zod: ^3.20.2 + '@better-auth/core@1.5.6': + resolution: {integrity: sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==} + peerDependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@cloudflare/workers-types': '>=4' + '@opentelemetry/api': ^1.9.0 + better-call: 1.3.2 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + '@better-auth/drizzle-adapter@1.5.6': + resolution: {integrity: sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + drizzle-orm: '>=0.41.0' + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.5.6': + resolution: {integrity: sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + kysely: ^0.27.0 || ^0.28.0 + peerDependenciesMeta: + kysely: + optional: true + + '@better-auth/memory-adapter@1.5.6': + resolution: {integrity: sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + + '@better-auth/mongo-adapter@1.5.6': + resolution: {integrity: sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + + '@better-auth/prisma-adapter@1.5.6': + resolution: {integrity: sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==} + peerDependencies: + '@better-auth/core': 1.5.6 + '@better-auth/utils': ^0.3.0 + '@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 + + '@better-auth/telemetry@1.5.6': + resolution: {integrity: sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==} + peerDependencies: + '@better-auth/core': 1.5.6 + + '@better-auth/utils@0.3.1': + resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==} + + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@biomejs/biome@2.3.4': resolution: {integrity: sha512-TU08LXjBHdy0mEY9APtEtZdNQQijXUDSXR7IK1i45wgoPD5R0muK7s61QcFir6FpOj/RP1+YkPx5QJlycXUU3w==} engines: {node: '>=14.21.3'} @@ -72,24 +153,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.3.4': resolution: {integrity: sha512-y7efHyyM2gYmHy/AdWEip+VgTMe9973aP7XYKPzu/j8JxnPHuSUXftzmPhkVw0lfm4ECGbdBdGD6+rLmTgNZaA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.3.4': resolution: {integrity: sha512-mzKFFv/w66e4/jCobFmD3kymCqG+FuWE7sVa4Yjqd9v7qt2UhXo67MSZKY9Ih18V2IwPzRKQPCw6KwdZs6AXSA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.3.4': resolution: {integrity: sha512-gKfjWR/6/dfIxPJCw8REdEowiXCkIpl9jycpNVHux8aX2yhWPLjydOshkDL6Y/82PcQJHn95VCj7J+BRcE5o1Q==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.3.4': resolution: {integrity: sha512-5TJ6JfVez+yyupJ/iGUici2wzKf0RrSAxJhghQXtAEsc67OIpdwSKAQboemILrwKfHDi5s6mu7mX+VTCTUydkw==} @@ -332,67 +417,79 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -421,6 +518,22 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + '@poppinss/colors@4.1.5': resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==} @@ -455,6 +568,9 @@ packages: '@speed-highlight/core@1.2.12': resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/node@20.8.3': resolution: {integrity: sha512-jxiZQFpb+NlH5kjW49vXxvxTjeeqlbsnTAdBTKpzEdPs9itay7MscYXz3Fo9VYFEsfQ6LJFitHad3faerLAjCw==} @@ -473,6 +589,76 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + better-auth@1.5.6: + resolution: {integrity: sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==} + 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.41.0' + 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 + + better-call@1.3.2: + resolution: {integrity: sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -498,6 +684,9 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -529,6 +718,9 @@ packages: is-arrayish@0.3.4: resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -542,6 +734,10 @@ packages: peerDependencies: kysely: '*' + kysely@0.28.14: + resolution: {integrity: sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==} + engines: {node: '>=20.0.0'} + kysely@0.28.8: resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==} engines: {node: '>=20.0.0'} @@ -561,6 +757,10 @@ packages: engines: {node: ^18 || >=20} hasBin: true + nanostores@1.2.0: + resolution: {integrity: sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==} + engines: {node: ^20.0.0 || >=22.0.0} + openapi3-ts@4.5.0: resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} @@ -570,11 +770,17 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + sharp@0.33.5: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -659,6 +865,9 @@ packages: zod@4.1.11: resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: '@asteasolutions/zod-to-openapi@7.3.4(zod@3.25.76)': @@ -666,6 +875,80 @@ snapshots: openapi3-ts: 4.5.0 zod: 3.25.76 + '@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0)': + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + '@standard-schema/spec': 1.1.0 + better-call: 1.3.2(zod@4.3.6) + jose: 6.2.2 + kysely: 0.28.8 + nanostores: 1.2.0 + zod: 4.3.6 + optionalDependencies: + '@cloudflare/workers-types': 4.20251107.0 + + '@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)': + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + '@standard-schema/spec': 1.1.0 + better-call: 1.3.2(zod@4.3.6) + jose: 6.2.2 + kysely: 0.28.14 + nanostores: 1.2.0 + zod: 4.3.6 + optionalDependencies: + '@cloudflare/workers-types': 4.20251107.0 + + '@better-auth/drizzle-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + + '@better-auth/kysely-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + optionalDependencies: + kysely: 0.28.14 + + '@better-auth/kysely-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.8)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + optionalDependencies: + kysely: 0.28.8 + + '@better-auth/memory-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + + '@better-auth/mongo-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + + '@better-auth/prisma-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + + '@better-auth/telemetry@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))': + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.3.1': {} + + '@better-fetch/fetch@1.1.21': {} + '@biomejs/biome@2.3.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.3.4 @@ -896,6 +1179,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@noble/ciphers@2.1.1': {} + + '@noble/hashes@2.0.1': {} + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/semantic-conventions@1.40.0': {} + '@poppinss/colors@4.1.5': dependencies: kleur: 4.1.5 @@ -932,6 +1223,8 @@ snapshots: '@speed-highlight/core@1.2.12': {} + '@standard-schema/spec@1.1.0': {} + '@types/node@20.8.3': {} '@types/service-worker-mock@2.0.4': {} @@ -942,6 +1235,38 @@ snapshots: argparse@2.0.1: {} + better-auth@1.5.6(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1): + dependencies: + '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) + '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14) + '@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251107.0)(@opentelemetry/api@1.9.1)(better-call@1.3.2(zod@3.25.76))(jose@6.2.2)(kysely@0.28.8)(nanostores@1.2.0)) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.3.2(zod@4.3.6) + defu: 6.1.4 + jose: 6.2.2 + kysely: 0.28.14 + nanostores: 1.2.0 + zod: 4.3.6 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - '@opentelemetry/api' + + better-call@1.3.2(zod@4.3.6): + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 3.1.0 + optionalDependencies: + zod: 4.3.6 + blake3-wasm@2.1.5: {} chanfana@2.8.3: @@ -970,6 +1295,8 @@ snapshots: cookie@1.0.2: {} + defu@6.1.4: {} + detect-libc@2.1.2: {} error-stack-parser-es@1.0.5: {} @@ -1013,6 +1340,8 @@ snapshots: is-arrayish@0.3.4: {} + jose@6.2.2: {} + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -1023,6 +1352,8 @@ snapshots: dependencies: kysely: 0.28.8 + kysely@0.28.14: {} + kysely@0.28.8: {} mime@3.0.0: {} @@ -1047,6 +1378,8 @@ snapshots: nanoid@5.1.5: {} + nanostores@1.2.0: {} + openapi3-ts@4.5.0: dependencies: yaml: 2.8.1 @@ -1055,8 +1388,12 @@ snapshots: pathe@2.0.3: {} + rou3@0.7.12: {} + semver@7.7.3: {} + set-cookie-parser@3.1.0: {} + sharp@0.33.5: dependencies: color: 4.2.3 @@ -1155,3 +1492,5 @@ snapshots: zod@3.25.76: {} zod@4.1.11: {} + + zod@4.3.6: {} diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..6c2cd69 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,37 @@ +import { betterAuth } from "better-auth"; +import { kyselyAdapter } from "@better-auth/kysely-adapter"; +import { bearer } from "better-auth/plugins"; +import { genericOAuth } from "better-auth/plugins"; +import { Kysely } from "kysely"; +import { D1Dialect } from "kysely-d1"; +import type { Env } from "../worker-configuration"; + +/** + * 每次请求时创建 better-auth 实例。 + * Cloudflare Workers 的 D1 binding 仅在请求上下文中可用, + * 因此无法在模块顶层初始化,必须在请求处理器内调用此工厂函数。 + */ +export function createAuth(env: Env) { + const db = new Kysely({ + dialect: new D1Dialect({ database: env.ACTIVE_DB }), + }); + + return betterAuth({ + database: kyselyAdapter(db, { type: "sqlite" }), + secret: env.BETTER_AUTH_SECRET, + plugins: [ + bearer(), + genericOAuth({ + config: [ + { + providerId: "logto", + clientId: env.LOGTO_CLIENT_ID, + clientSecret: env.LOGTO_CLIENT_SECRET, + discoveryUrl: `${env.LOGTO_ISSUER}/.well-known/openid-configuration`, + pkce: true, + }, + ], + }), + ], + }); +} diff --git a/src/index.ts b/src/index.ts index 0c5b402..864de8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,9 +5,28 @@ import { FreshmanAdd } from "./endpoints/freshmanAdd"; // import { FreshmanFetch } from "./endpoints/freshmanFetch"; import { FreshmanList } from "./endpoints/freshmanList"; import { Scalar } from "@scalar/hono-api-reference"; +import { createAuth } from "./auth"; +import type { Env } from "../worker-configuration"; // Start a Hono app -const app = new Hono(); +const app = new Hono<{ Bindings: Env }>(); + +// better-auth: 处理所有 /api/auth/* 路由(登录、回调、登出、session 等) +app.all("/api/auth/*", async (c) => { + const auth = createAuth(c.env); + return auth.handler(c.req.raw); +}); + +// 保护 GET /api/freshman:要求有效的 session(支持 Bearer Token 和 Cookie) +app.use("/api/freshman", async (c, next) => { + if (c.req.method !== "GET") return next(); + const auth = createAuth(c.env); + const session = await auth.api.getSession({ headers: c.req.raw.headers }); + if (!session) { + return c.json({ error: "Unauthorized" }, 401); + } + return next(); +}); // Setup OpenAPI registry app.use("*", async (c, next) => { diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 0274a31..2da767e 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -7,6 +7,11 @@ declare namespace Cloudflare { } interface Env { ACTIVE_DB: D1Database; + BETTER_AUTH_SECRET: string; + LOGTO_CLIENT_ID: string; + LOGTO_CLIENT_SECRET: string; + LOGTO_ISSUER: string; + LOGTO_CALLBACK_URL: string; } } interface Env extends Cloudflare.Env {} diff --git a/wrangler.jsonc b/wrangler.jsonc index afbadbd..42cb7ac 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -7,10 +7,14 @@ "name": "active", "main": "src/index.ts", "compatibility_date": "2025-09-03", + "compatibility_flags": ["nodejs_compat"], "observability": { "enabled": true }, - "vars": {}, + "vars": { + "LOGTO_ISSUER": "", + "LOGTO_CALLBACK_URL": "" + }, "secrets_store_secrets": [], "d1_databases": [ {