Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dist"
],
"scripts": {
"prepare": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
1 change: 1 addition & 0 deletions packages/error-handler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dist"
],
"scripts": {
"prepare": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
1 change: 1 addition & 0 deletions packages/firebase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"types": "./dist/types/index.d.ts",
"files": ["dist"],
"scripts": {
"prepare": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
1 change: 1 addition & 0 deletions packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"types": "./dist/types/index.d.ts",
"files": ["dist"],
"scripts": {
"prepare": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
1 change: 1 addition & 0 deletions packages/mailer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"types": "./dist/types/index.d.ts",
"files": ["dist"],
"scripts": {
"prepare": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
1 change: 1 addition & 0 deletions packages/s3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"types": "./dist/types/index.d.ts",
"files": ["dist"],
"scripts": {
"prepare": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
1 change: 1 addition & 0 deletions packages/slonik/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"types": "./dist/types/index.d.ts",
"files": ["dist"],
"scripts": {
"prepare": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
1 change: 1 addition & 0 deletions packages/swagger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dist"
],
"scripts": {
"prepare": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand Down
9 changes: 8 additions & 1 deletion packages/user/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
"main": "./dist/prefabs-tech-fastify-user.cjs",
"module": "./dist/prefabs-tech-fastify-user.js",
"types": "./dist/types/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"prepare": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
Expand All @@ -29,7 +32,10 @@
"typecheck": "tsc --noEmit -p tsconfig.json --composite false"
},
"dependencies": {
"better-auth": "1.2.7",
"humps": "2.0.1",
"kysely": "^0.27.6",
"pg": "^8.20.0",
"validator": "13.15.26"
},
"devDependencies": {
Expand All @@ -43,6 +49,7 @@
"@prefabs.tech/tsconfig": "0.5.0",
"@types/humps": "2.0.6",
"@types/node": "24.10.13",
"@types/pg": "^8.20.0",
"@types/validator": "13.15.10",
"@vitest/coverage-istanbul": "3.2.4",
"eslint": "9.39.2",
Expand Down
69 changes: 69 additions & 0 deletions packages/user/src/auth/authProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { FastifyInstance, FastifyRequest } from "fastify";

export interface AuthUser {
id: string;
email: string;
emailVerified: boolean;
roles: string[];
metadata?: Record<string, unknown>;
}

export interface Session {
sessionId: string;
userId: string;
roles: string[];
expiresAt: Date;
}

export interface AppError {
code: string;
message: string;
statusCode: number;
cause?: unknown;
}

export type AuthCapability = "phoneOtp" | "passkey" | "oauth";

export interface AuthProvider {
bootstrap(fastify: FastifyInstance): Promise<void>;

// --- Email + password (universal) ---
signUp(
email: string,
password: string,
): Promise<{ user: AuthUser; token: string }>;
signIn(
email: string,
password: string,
): Promise<{ user: AuthUser; token: string }>;
updateEmail(userId: string, newEmail: string): Promise<void>;
updatePassword(
userId: string,
currentPassword: string,
newPassword: string,
): Promise<void>;
sendVerificationEmail(email: string): Promise<void>;
verifyEmail(token: string): Promise<{ userId: string }>;
sendPasswordResetEmail(email: string): Promise<void>;
resetPassword(token: string, newPassword: string): Promise<void>;

// --- Session ---
verifySession(req: FastifyRequest): Promise<Session | undefined>;
signOut(req: FastifyRequest): Promise<unknown>;
revokeAllSessions(userId: string): Promise<void>;

// --- User & roles ---
getUser(id: string): Promise<AuthUser | undefined>;
getUserRoles(userId: string): Promise<string[]>;
assignRoles(userId: string, roles: string[]): Promise<void>;
removeRoles(userId: string, roles: string[]): Promise<void>;

// --- Error normalization ---
normalizeError(err: unknown): AppError;

// --- Capability registry ---
supports(capability: AuthCapability): boolean;
readonly phoneOtp: import("./capabilities").PhoneOtpCapability | undefined;
readonly passkey: import("./capabilities").PasskeyCapability | undefined;
readonly oauth: import("./capabilities").OAuthCapability | undefined;
}
78 changes: 78 additions & 0 deletions packages/user/src/auth/better-auth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { betterAuth } from "better-auth";
import { admin } from "better-auth/plugins/admin";
import { bearer } from "better-auth/plugins/bearer";
import { phoneNumber } from "better-auth/plugins/phone-number";
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";

import type { BetterAuthConfig } from "../../types/config";

/**
* Creates the Better Auth instance from config.
*
* connectionString is derived from fastify.config.slonik.db via stringifyDsn
* in plugin.ts — the same DB that slonik already connects to. No separate
* DB configuration needed.
*
* Our application queries (getUser, roles) go through slonik (fastify.slonik).
* better-auth manages its own tables internally (user, session, account, verification).
*
* POC notes:
* - sendOTP logs to console — replace with fastify.mailer in Phase 3
* - sendResetPassword logs to console — same
* - admin plugin required for revokeUserSessions(userId)
* - passkey not in better-auth v1.x — tracked in findings.md
*/
export function createAuth(config: BetterAuthConfig, connectionString: string) {
// Create a Kysely instance with a pg Pool that better-auth will use.
// This satisfies better-auth's expectation for a Kysely adapter.
const pool = new Pool({ connectionString });
const db = new Kysely({
dialect: new PostgresDialect({ pool }),
});

return betterAuth({
database: {
db: db,
type: "postgres",
},

basePath: config.routePrefix ?? "/auth",

secret: config.secret,

trustedOrigins: config.trustedOrigins ?? [],

emailAndPassword: {
enabled: true,
sendResetPassword: async ({ user, url }) => {
// POC: log to console. Phase 3: use fastify.mailer
console.log(
`[BetterAuth POC] Password reset for ${user.email} → ${url}`,
);
},
sendEmailVerificationOnSignUp: false,
},

plugins: [
// POC assumption #4 — phone OTP capability
bearer(),
phoneNumber({
sendOTP: async ({ phoneNumber: phone, code }) => {
// POC: log to console. Phase 3: use SMS provider via fastify
console.log(`[BetterAuth POC] OTP for ${phone}: ${code}`);
},
expiresIn: 300,
signUpOnVerification: {
getTempEmail: (phone) => `${phone.replaceAll("+", "")}@phone.local`,
getTempName: (phone) => phone,
},
}),

// Required for revokeAllSessions(userId)
admin(),
],
});
}

export type Auth = ReturnType<typeof createAuth>;
Loading
Loading