From 31eea207ba311045a1776593399d8e69d234e9ba Mon Sep 17 00:00:00 2001 From: luciano Date: Wed, 29 Apr 2026 12:48:51 +0200 Subject: [PATCH] feat(auth): add @ketopath/auth package wrapping Better Auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New workspace packages/auth exporting: - auth: Better Auth server instance using Prisma adapter on @ketopath/db - makeAuthClient: React client factory parameterised by baseURL - readAuthEnv: Zod-validated env reader (BETTER_AUTH_SECRET min 32 chars, BETTER_AUTH_URL, optional GOOGLE_CLIENT_ID/SECRET) - emailAndPassword enabled with min 8 chars, requireEmailVerification: false for MVP (re-enable in V1, see ADR 0001) - Google provider conditionally registered when both env vars are present — keeps the package usable before OAuth configuration is delivered - 7-day sessions, 24h rolling refresh, cookie prefix 'ketopath' Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/auth/.eslintrc.cjs | 5 +++++ packages/auth/package.json | 25 ++++++++++++++++++++++ packages/auth/src/client.ts | 5 +++++ packages/auth/src/env.ts | 19 +++++++++++++++++ packages/auth/src/index.ts | 2 ++ packages/auth/src/server.ts | 40 ++++++++++++++++++++++++++++++++++++ packages/auth/tsconfig.json | 12 +++++++++++ pnpm-lock.yaml | Bin 203349 -> 217082 bytes tsconfig.json | 3 ++- 9 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 packages/auth/.eslintrc.cjs create mode 100644 packages/auth/package.json create mode 100644 packages/auth/src/client.ts create mode 100644 packages/auth/src/env.ts create mode 100644 packages/auth/src/index.ts create mode 100644 packages/auth/src/server.ts create mode 100644 packages/auth/tsconfig.json diff --git a/packages/auth/.eslintrc.cjs b/packages/auth/.eslintrc.cjs new file mode 100644 index 0000000..2aed780 --- /dev/null +++ b/packages/auth/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + extends: ['@ketopath/eslint-config'], +}; diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 0000000..acd5921 --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,25 @@ +{ + "name": "@ketopath/auth", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./client": "./src/client.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint src --max-warnings=0" + }, + "dependencies": { + "@ketopath/db": "workspace:*", + "better-auth": "^1.0.21", + "zod": "^3.23.8" + }, + "devDependencies": { + "@ketopath/eslint-config": "workspace:*", + "@ketopath/tsconfig": "workspace:*" + } +} diff --git a/packages/auth/src/client.ts b/packages/auth/src/client.ts new file mode 100644 index 0000000..969b94e --- /dev/null +++ b/packages/auth/src/client.ts @@ -0,0 +1,5 @@ +import { createAuthClient } from 'better-auth/react'; + +export function makeAuthClient(baseURL: string): ReturnType { + return createAuthClient({ baseURL }); +} diff --git a/packages/auth/src/env.ts b/packages/auth/src/env.ts new file mode 100644 index 0000000..154017d --- /dev/null +++ b/packages/auth/src/env.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +// Le env vars sono lette dal processo che importa @ketopath/auth (apps/web, +// apps/api, ecc.). Vengono validate al momento della creazione dell'istanza, +// così errori di configurazione emergono al boot e non a runtime. +const authEnvSchema = z.object({ + BETTER_AUTH_SECRET: z + .string() + .min(32, 'BETTER_AUTH_SECRET deve essere lungo almeno 32 caratteri'), + BETTER_AUTH_URL: z.string().url(), + GOOGLE_CLIENT_ID: z.string().optional(), + GOOGLE_CLIENT_SECRET: z.string().optional(), +}); + +export type AuthEnv = z.infer; + +export function readAuthEnv(source: NodeJS.ProcessEnv = process.env): AuthEnv { + return authEnvSchema.parse(source); +} diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts new file mode 100644 index 0000000..54f39cc --- /dev/null +++ b/packages/auth/src/index.ts @@ -0,0 +1,2 @@ +export { auth, type Auth } from './server.js'; +export { readAuthEnv, type AuthEnv } from './env.js'; diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts new file mode 100644 index 0000000..f1cd296 --- /dev/null +++ b/packages/auth/src/server.ts @@ -0,0 +1,40 @@ +import { prisma } from '@ketopath/db'; +import { betterAuth } from 'better-auth'; +import { prismaAdapter } from 'better-auth/adapters/prisma'; + +import { readAuthEnv } from './env.js'; + +const env = readAuthEnv(); + +const googleProvider = + env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET + ? { + google: { + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET, + }, + } + : undefined; + +export const auth = betterAuth({ + database: prismaAdapter(prisma, { provider: 'postgresql' }), + secret: env.BETTER_AUTH_SECRET, + baseURL: env.BETTER_AUTH_URL, + emailAndPassword: { + enabled: true, + // MVP: nessuna verifica email per non bloccare l'onboarding (vedi + // docs/decisions/0001-auth-provider.md). Verrà attivata in V1. + requireEmailVerification: false, + minPasswordLength: 8, + }, + socialProviders: googleProvider, + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 giorni + updateAge: 60 * 60 * 24, // refresh expiresAt ogni 24h se la session è attiva + }, + advanced: { + cookiePrefix: 'ketopath', + }, +}); + +export type Auth = typeof auth; diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 0000000..79c7de1 --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@ketopath/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "composite": true, + "lib": ["DOM", "ES2022"], + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 116cec01f65d803271025f45024f0bb3a85d4202..8cf2904fb5cf77a24ad020fec2f6736599170855 100644 GIT binary patch delta 10219 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 diff --git a/tsconfig.json b/tsconfig.json index 2425989..ff7b12f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "./packages/shared" }, { "path": "./packages/ui" }, - { "path": "./packages/db" } + { "path": "./packages/db" }, + { "path": "./packages/auth" } ] }