diff --git a/packages/better-auth-types/package.json b/packages/better-auth-types/package.json new file mode 100644 index 000000000..0d5401004 --- /dev/null +++ b/packages/better-auth-types/package.json @@ -0,0 +1,35 @@ +{ + "name": "@manacore/better-auth-types", + "version": "1.0.0", + "description": "Centralized Better Auth type definitions for the Mana Core monorepo", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "type-check": "tsc --noEmit" + }, + "peerDependencies": { + "better-auth": "^1.4.0" + }, + "peerDependenciesMeta": { + "better-auth": { + "optional": true + } + }, + "devDependencies": { + "typescript": "^5.7.2" + }, + "keywords": [ + "manacore", + "better-auth", + "types", + "authentication", + "jwt" + ], + "license": "MIT" +} diff --git a/packages/better-auth-types/src/index.ts b/packages/better-auth-types/src/index.ts new file mode 100644 index 000000000..262728eb1 --- /dev/null +++ b/packages/better-auth-types/src/index.ts @@ -0,0 +1,341 @@ +/** + * @manacore/better-auth-types + * + * Centralized Better Auth type definitions for the Mana Core monorepo. + * + * This package provides: + * - Shared types for server and client + * - Field definitions for client type inference + * - Type guards and utilities + * + * @example Server-side (NestJS backends) + * ```typescript + * import type { JWTPayload, UserRole } from '@manacore/better-auth-types'; + * ``` + * + * @example Client-side (web/mobile apps) + * ```typescript + * import { createAuthClient } from "better-auth/client"; + * import { inferAdditionalFields } from "better-auth/client/plugins"; + * import { userAdditionalFields } from "@manacore/better-auth-types"; + * + * export const authClient = createAuthClient({ + * baseURL: "http://localhost:3001", + * plugins: [inferAdditionalFields(userAdditionalFields)], + * }); + * ``` + * + * @see https://www.better-auth.com/docs/concepts/typescript + */ + +// ============================================================================= +// User Role Types +// ============================================================================= + +/** + * User role type + * + * Valid roles in the Mana Core system: + * - 'user': Standard user (default) + * - 'admin': Administrator with elevated privileges + * - 'service': Service account for automated systems + */ +export type UserRole = 'user' | 'admin' | 'service'; + +/** + * Type guard to check if a value is a valid UserRole + */ +export function isValidUserRole(role: unknown): role is UserRole { + return typeof role === 'string' && ['user', 'admin', 'service'].includes(role); +} + +// ============================================================================= +// Organization Role Types +// ============================================================================= + +/** + * Organization role type (from Better Auth organization plugin) + */ +export type OrganizationRole = 'owner' | 'admin' | 'member'; + +/** + * Type guard to check if a value is a valid OrganizationRole + */ +export function isValidOrganizationRole(role: unknown): role is OrganizationRole { + return typeof role === 'string' && ['owner', 'admin', 'member'].includes(role); +} + +/** + * Invitation status for organization invitations + */ +export type InvitationStatus = 'pending' | 'accepted' | 'rejected' | 'expired'; + +// ============================================================================= +// JWT Token Types +// ============================================================================= + +/** + * JWT token payload structure + * + * Matches the payload defined in mana-core-auth's better-auth.config.ts + * + * @see services/mana-core-auth/src/auth/better-auth.config.ts + */ +export interface JWTPayload { + /** User ID (JWT subject) */ + sub: string; + + /** User email */ + email: string; + + /** User role (user, admin, service) */ + role: string; + + /** Session ID (primary field) */ + sid: string; + + /** Session ID (alias for backward compatibility) */ + sessionId?: string; + + /** Expiration time (Unix timestamp) */ + exp?: number; + + /** Issued at time (Unix timestamp) */ + iat?: number; + + /** Issuer */ + iss?: string; + + /** Audience */ + aud?: string; +} + +/** + * Strictly typed JWT payload with UserRole instead of string + */ +export interface StrictJWTPayload extends Omit { + role: UserRole; +} + +// ============================================================================= +// User Data Types +// ============================================================================= + +/** + * User data extracted from JWT token + * Used by @manacore/shared-auth clients + */ +export interface UserData { + id: string; + sub: string; + email: string; + role: string; +} + +/** + * Strictly typed user data with UserRole instead of string + */ +export interface StrictUserData extends Omit { + role: UserRole; +} + +/** + * Type helper for users with additional fields + */ +export type UserWithAdditionalFields = { + role: string; +}; + +// ============================================================================= +// Authentication Result Types +// ============================================================================= + +/** + * Authentication result from sign in/up + */ +export interface AuthResult { + success: boolean; + error?: string; + needsVerification?: boolean; +} + +/** + * Token refresh result + */ +export interface TokenRefreshResult { + appToken: string; + refreshToken: string; + userData?: UserData | null; +} + +// ============================================================================= +// Better Auth Client Field Definitions +// ============================================================================= + +/** + * Additional field definitions for Better Auth clients + * + * These definitions are used by the `inferAdditionalFields` plugin + * to provide proper TypeScript inference for custom user fields. + * + * IMPORTANT: Keep this in sync with the server config in + * services/mana-core-auth/src/auth/better-auth.config.ts + * + * @example + * ```typescript + * import { createAuthClient } from "better-auth/client"; + * import { inferAdditionalFields } from "better-auth/client/plugins"; + * import { userAdditionalFields } from "@manacore/better-auth-types"; + * + * export const authClient = createAuthClient({ + * baseURL: "http://localhost:3001", + * plugins: [inferAdditionalFields(userAdditionalFields)], + * }); + * + * // Now user.role is properly typed! + * const session = await authClient.getSession(); + * console.log(session?.user.role); // TypeScript knows this is string + * ``` + */ +export const userAdditionalFields = { + user: { + /** + * User role (user, admin, service) + * + * This field is read-only on the client (input: false on server). + * Roles can only be assigned server-side. + */ + role: { + type: 'string' as const, + }, + }, +} as const; + +// ============================================================================= +// Organization Types +// ============================================================================= + +/** + * Organization data structure + */ +export interface Organization { + id: string; + name: string; + slug: string; + logo?: string | null; + metadata?: Record | null; + createdAt: Date; +} + +/** + * Organization member data + */ +export interface OrganizationMember { + id: string; + userId: string; + organizationId: string; + role: OrganizationRole; + createdAt: Date; +} + +/** + * Organization invitation data + */ +export interface OrganizationInvitation { + id: string; + organizationId: string; + email: string; + role: OrganizationRole; + status: InvitationStatus; + expiresAt: Date; + inviterId: string; +} + +// ============================================================================= +// NestJS Integration Types +// ============================================================================= + +/** + * User data extracted from JWT for NestJS guards + * Used by @manacore/shared-nestjs-auth + */ +export interface CurrentUserData { + userId: string; + email: string; + role: string; + sessionId?: string; +} + +/** + * Strictly typed current user data + */ +export interface StrictCurrentUserData extends Omit { + role: UserRole; +} + +/** + * Convert JWT payload to CurrentUserData + */ +export function jwtPayloadToCurrentUser(payload: JWTPayload): CurrentUserData { + return { + userId: payload.sub, + email: payload.email, + role: payload.role, + sessionId: payload.sid, + }; +} + +/** + * Convert JWT payload to strictly typed CurrentUserData (validates role) + */ +export function jwtPayloadToStrictCurrentUser(payload: JWTPayload): StrictCurrentUserData | null { + if (!isValidUserRole(payload.role)) { + return null; + } + return { + userId: payload.sub, + email: payload.email, + role: payload.role, + sessionId: payload.sid, + }; +} + +// ============================================================================= +// Token Validation Types +// ============================================================================= + +/** + * Response from token validation endpoint + */ +export interface TokenValidationResponse { + valid: boolean; + payload?: JWTPayload; + error?: string; +} + +// ============================================================================= +// Credit Types +// ============================================================================= + +/** + * Credit balance information + */ +export interface CreditBalance { + credits: number; + maxCreditLimit: number; + userId: string; +} + +// ============================================================================= +// B2B Types +// ============================================================================= + +/** + * B2B information from JWT claims + */ +export interface B2BInfo { + disableRevenueCat: boolean; + organizationId?: string; + plan?: string; + role?: string; +} diff --git a/packages/better-auth-types/tsconfig.json b/packages/better-auth-types/tsconfig.json new file mode 100644 index 000000000..cff9cf519 --- /dev/null +++ b/packages/better-auth-types/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/shared-auth/package.json b/packages/shared-auth/package.json index 661f49e4d..0353d2a8a 100644 --- a/packages/shared-auth/package.json +++ b/packages/shared-auth/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@manacore/shared-types": "workspace:*", + "@manacore/better-auth-types": "workspace:*", "base64-js": "^1.5.1" }, "devDependencies": { diff --git a/packages/shared-auth/src/better-auth-fields.ts b/packages/shared-auth/src/better-auth-fields.ts new file mode 100644 index 000000000..6aa3b1e7f --- /dev/null +++ b/packages/shared-auth/src/better-auth-fields.ts @@ -0,0 +1,35 @@ +/** + * Better Auth Additional Field Definitions + * + * Re-exports centralized types from @manacore/better-auth-types. + * This file is provided for convenience when importing from @manacore/shared-auth. + * + * @example + * ```typescript + * // In SvelteKit/React app + * import { createAuthClient } from "better-auth/client"; + * import { inferAdditionalFields } from "better-auth/client/plugins"; + * import { userAdditionalFields } from "@manacore/shared-auth"; + * + * export const authClient = createAuthClient({ + * baseURL: "http://localhost:3001", + * plugins: [inferAdditionalFields(userAdditionalFields)], + * }); + * + * // Now user.role is properly typed! + * const session = await authClient.getSession(); + * console.log(session?.user.role); // TypeScript knows this is string + * ``` + * + * @see https://www.better-auth.com/docs/concepts/typescript + */ + +// Re-export from centralized package +export { + userAdditionalFields, + type UserRole, + type OrganizationRole, + type UserWithAdditionalFields, + isValidUserRole, + isValidOrganizationRole, +} from '@manacore/better-auth-types'; diff --git a/packages/shared-auth/src/index.ts b/packages/shared-auth/src/index.ts index 77db0d5f9..10a56503c 100644 --- a/packages/shared-auth/src/index.ts +++ b/packages/shared-auth/src/index.ts @@ -1,6 +1,16 @@ // Types export * from './types'; +// Better Auth field definitions for client type inference +export { + userAdditionalFields, + type UserRole, + type OrganizationRole, + type UserWithAdditionalFields, + isValidUserRole, + isValidOrganizationRole, +} from './better-auth-fields'; + // Core utilities import { createAuthService as _createAuthService } from './core/authService'; export { createAuthService } from './core/authService'; diff --git a/packages/shared-nestjs-auth/package.json b/packages/shared-nestjs-auth/package.json index 339236f23..b6cc5dc1f 100644 --- a/packages/shared-nestjs-auth/package.json +++ b/packages/shared-nestjs-auth/package.json @@ -8,11 +8,15 @@ "build": "tsc", "clean": "rm -rf dist", "prepublishOnly": "pnpm build", - "lint": "eslint ." + "lint": "eslint .", + "type-check": "tsc --noEmit" }, "files": [ "dist" ], + "dependencies": { + "@manacore/better-auth-types": "workspace:*" + }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/config": "^3.0.0 || ^4.0.0" diff --git a/packages/shared-nestjs-auth/src/types/index.ts b/packages/shared-nestjs-auth/src/types/index.ts index 62e39dc5a..de57ed96b 100644 --- a/packages/shared-nestjs-auth/src/types/index.ts +++ b/packages/shared-nestjs-auth/src/types/index.ts @@ -1,12 +1,27 @@ /** - * User data extracted from JWT token + * NestJS Auth Types + * + * Re-exports centralized types from @manacore/better-auth-types + * plus NestJS-specific configuration interfaces. */ -export interface CurrentUserData { - userId: string; - email: string; - role: string; - sessionId?: string; -} + +// Re-export centralized types +export type { + CurrentUserData, + StrictCurrentUserData, + JWTPayload, + StrictJWTPayload, + UserRole, + OrganizationRole, + TokenValidationResponse, +} from '@manacore/better-auth-types'; + +export { + isValidUserRole, + isValidOrganizationRole, + jwtPayloadToCurrentUser, + jwtPayloadToStrictCurrentUser, +} from '@manacore/better-auth-types'; /** * Configuration for the auth module @@ -19,22 +34,3 @@ export interface AuthModuleConfig { /** Test user ID for development mode */ devUserId?: string; } - -/** - * Response from token validation endpoint - */ -export interface TokenValidationResponse { - valid: boolean; - payload?: { - sub: string; - email: string; - role: string; - sessionId?: string; - sid?: string; - iat?: number; - exp?: number; - iss?: string; - aud?: string; - }; - error?: string; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f78a7831..9a96ca660 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3782,6 +3782,16 @@ importers: specifier: ^5.0.0 version: 5.9.3 + packages/better-auth-types: + dependencies: + better-auth: + specifier: ^1.4.0 + version: 1.4.4(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(svelte@5.44.0) + devDependencies: + typescript: + specifier: ^5.7.2 + version: 5.9.3 + packages/eslint-config: dependencies: '@eslint/js': @@ -3926,6 +3936,9 @@ importers: packages/shared-auth: dependencies: + '@manacore/better-auth-types': + specifier: workspace:* + version: link:../better-auth-types '@manacore/shared-types': specifier: workspace:* version: link:../shared-types @@ -4184,6 +4197,10 @@ importers: version: 5.9.3 packages/shared-nestjs-auth: + dependencies: + '@manacore/better-auth-types': + specifier: workspace:* + version: link:../better-auth-types devDependencies: '@nestjs/common': specifier: ^10.0.0 @@ -21527,7 +21544,7 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0)': + '@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@4.2.0))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 @@ -21538,9 +21555,9 @@ snapshots: nanostores: 1.1.0 zod: 4.2.0 - '@better-auth/telemetry@1.4.4(@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0))': + '@better-auth/telemetry@1.4.4(@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@4.2.0))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0))': dependencies: - '@better-auth/core': 1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0) + '@better-auth/core': 1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@4.2.0))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 @@ -29973,8 +29990,8 @@ snapshots: better-auth@1.4.4(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(svelte@5.44.0): dependencies: - '@better-auth/core': 1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.4(@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@3.25.76))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0)) + '@better-auth/core': 1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@4.2.0))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.4(@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@4.2.0))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0)) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.18 '@noble/ciphers': 2.0.1 @@ -29992,6 +30009,27 @@ snapshots: react-dom: 19.1.0(react@19.1.0) svelte: 5.44.0 + better-auth@1.4.4(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(svelte@5.44.0): + dependencies: + '@better-auth/core': 1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@4.2.0))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.4(@better-auth/core@1.4.4(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.3(zod@4.2.0))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.1.0)) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.18 + '@noble/ciphers': 2.0.1 + '@noble/hashes': 2.0.1 + '@standard-schema/spec': 1.0.0 + better-call: 1.1.3(zod@4.2.0) + defu: 6.1.4 + jose: 6.1.2 + kysely: 0.28.8 + nanostores: 1.1.0 + zod: 4.2.0 + optionalDependencies: + '@sveltejs/kit': 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + svelte: 5.44.0 + better-call@1.1.3(zod@4.2.0): dependencies: '@better-auth/utils': 0.3.0