feat(auth): add centralized @manacore/better-auth-types package

Create new shared package for Better Auth type definitions:
- UserRole, JWTPayload, CurrentUserData types
- Type guards: isValidUserRole, isValidOrganizationRole
- Utility functions: jwtPayloadToCurrentUser
- userAdditionalFields for client type inference

Migrate shared packages to use centralized types:
- @manacore/shared-auth re-exports from new package
- @manacore/shared-nestjs-auth uses new package as dependency
This commit is contained in:
Wuesteon 2025-12-16 02:43:55 +01:00
parent eeab9b7fea
commit 26ca921158
9 changed files with 513 additions and 32 deletions

View file

@ -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"
}

View file

@ -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<JWTPayload, 'role'> {
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<UserData, 'role'> {
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<string, unknown> | 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<CurrentUserData, 'role'> {
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;
}

View file

@ -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"]
}

View file

@ -16,6 +16,7 @@
},
"dependencies": {
"@manacore/shared-types": "workspace:*",
"@manacore/better-auth-types": "workspace:*",
"base64-js": "^1.5.1"
},
"devDependencies": {

View file

@ -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';

View file

@ -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';

View file

@ -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"

View file

@ -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;
}

48
pnpm-lock.yaml generated
View file

@ -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