diff --git a/apps/website/app/api/auth/logout/route.ts b/apps/website/app/api/auth/logout/route.ts new file mode 100644 index 000000000..c2a4170e9 --- /dev/null +++ b/apps/website/app/api/auth/logout/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server'; +import { getAuthService } from '@/lib/auth'; + +export async function POST(request: Request) { + const authService = getAuthService(); + await authService.logout(); + + const url = new URL(request.url); + const redirectUrl = new URL('/', url.origin); + return NextResponse.redirect(redirectUrl); +} \ No newline at end of file diff --git a/apps/website/app/api/auth/session/route.ts b/apps/website/app/api/auth/session/route.ts new file mode 100644 index 000000000..e2f2dbac9 --- /dev/null +++ b/apps/website/app/api/auth/session/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server'; +import { getAuthService } from '@/lib/auth'; + +export async function GET() { + const authService = getAuthService(); + const session = await authService.getCurrentSession(); + + return NextResponse.json({ + session, + }); +} \ No newline at end of file diff --git a/apps/website/app/auth/iracing/callback/route.ts b/apps/website/app/auth/iracing/callback/route.ts index 8f64f98a5..cf465142e 100644 --- a/apps/website/app/auth/iracing/callback/route.ts +++ b/apps/website/app/auth/iracing/callback/route.ts @@ -3,7 +3,6 @@ import { NextResponse } from 'next/server'; import { getAuthService } from '../../../../lib/auth'; -const SESSION_COOKIE = 'gp_demo_session'; const STATE_COOKIE = 'gp_demo_auth_state'; export async function GET(request: Request) { @@ -24,14 +23,7 @@ export async function GET(request: Request) { } const authService = getAuthService(); - const session = await authService.loginWithIracingCallback({ code, state, returnTo }); - - cookieStore.set(SESSION_COOKIE, JSON.stringify(session), { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NODE_ENV === 'production', - }); + await authService.loginWithIracingCallback({ code, state, returnTo }); cookieStore.delete(STATE_COOKIE); diff --git a/apps/website/lib/auth/AuthService.ts b/apps/website/lib/auth/AuthService.ts index 34edea457..41ee284e7 100644 --- a/apps/website/lib/auth/AuthService.ts +++ b/apps/website/lib/auth/AuthService.ts @@ -1,17 +1,8 @@ -export interface AuthUser { - id: string; - displayName: string; - iracingCustomerId?: string; - primaryDriverId?: string; - avatarUrl?: string; -} +import type { AuthenticatedUserDTO } from '@gridpilot/identity/application/dto/AuthenticatedUserDTO'; +import type { AuthSessionDTO } from '@gridpilot/identity/application/dto/AuthSessionDTO'; -export interface AuthSession { - user: AuthUser; - issuedAt: number; - expiresAt: number; - token: string; -} +export type AuthUser = AuthenticatedUserDTO; +export type AuthSession = AuthSessionDTO; export interface AuthService { getCurrentSession(): Promise; diff --git a/apps/website/lib/auth/InMemoryAuthService.ts b/apps/website/lib/auth/InMemoryAuthService.ts index f41bfac2a..4f7df4a1b 100644 --- a/apps/website/lib/auth/InMemoryAuthService.ts +++ b/apps/website/lib/auth/InMemoryAuthService.ts @@ -1,59 +1,32 @@ -import { cookies } from 'next/headers'; -import { randomUUID } from 'crypto'; - -import type { AuthService, AuthSession, AuthUser } from './AuthService'; -import { createStaticRacingSeed } from '@gridpilot/testing-support'; - -const SESSION_COOKIE = 'gp_demo_session'; -const STATE_COOKIE = 'gp_demo_auth_state'; - -function parseCookieValue(raw: string | undefined): AuthSession | null { - if (!raw) return null; - try { - const parsed = JSON.parse(raw) as AuthSession; - if (!parsed.expiresAt || Date.now() > parsed.expiresAt) { - return null; - } - return parsed; - } catch { - return null; - } -} - -function serializeSession(session: AuthSession): string { - return JSON.stringify(session); -} +import type { AuthService, AuthSession } from './AuthService'; +import type { AuthCallbackCommandDTO } from '@gridpilot/identity/application/dto/AuthCallbackCommandDTO'; +import type { StartAuthCommandDTO } from '@gridpilot/identity/application/dto/StartAuthCommandDTO'; +import { StartAuthUseCase } from '@gridpilot/identity/application/use-cases/StartAuthUseCase'; +import { GetCurrentUserSessionUseCase } from '@gridpilot/identity/application/use-cases/GetCurrentUserSessionUseCase'; +import { HandleAuthCallbackUseCase } from '@gridpilot/identity/application/use-cases/HandleAuthCallbackUseCase'; +import { LogoutUseCase } from '@gridpilot/identity/application/use-cases/LogoutUseCase'; +import { CookieIdentitySessionAdapter } from '@gridpilot/identity/infrastructure/session/CookieIdentitySessionAdapter'; +import { IracingDemoIdentityProviderAdapter } from '@gridpilot/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter'; export class InMemoryAuthService implements AuthService { - private readonly seedDriverId: string; - - constructor() { - const seed = createStaticRacingSeed(42); - this.seedDriverId = seed.drivers[0]?.id ?? 'driver-1'; - } - async getCurrentSession(): Promise { - const store = await cookies(); - const raw = store.get(SESSION_COOKIE)?.value; - return parseCookieValue(raw); + const sessionPort = new CookieIdentitySessionAdapter(); + const useCase = new GetCurrentUserSessionUseCase(sessionPort); + return useCase.execute(); } async startIracingAuthRedirect( returnTo?: string, ): Promise<{ redirectUrl: string; state: string }> { - const state = randomUUID(); + const provider = new IracingDemoIdentityProviderAdapter(); + const useCase = new StartAuthUseCase(provider); - const params = new URLSearchParams(); - params.set('code', 'dummy-code'); - params.set('state', state); - if (returnTo) { - params.set('returnTo', returnTo); - } - - return { - redirectUrl: `/auth/iracing/callback?${params.toString()}`, - state, + const command: StartAuthCommandDTO = { + provider: 'IRACING_DEMO', + returnTo, }; + + return useCase.execute(command); } async loginWithIracingCallback(params: { @@ -61,36 +34,23 @@ export class InMemoryAuthService implements AuthService { state: string; returnTo?: string; }): Promise { - if (!params.code) { - throw new Error('Missing auth code'); - } - if (!params.state) { - throw new Error('Missing auth state'); - } + const provider = new IracingDemoIdentityProviderAdapter(); + const sessionPort = new CookieIdentitySessionAdapter(); + const useCase = new HandleAuthCallbackUseCase(provider, sessionPort); - const user: AuthUser = { - id: 'demo-user', - displayName: 'GridPilot Demo Driver', - iracingCustomerId: '000000', - primaryDriverId: this.seedDriverId, - avatarUrl: `/api/avatar/${this.seedDriverId}`, + const command: AuthCallbackCommandDTO = { + provider: 'IRACING_DEMO', + code: params.code, + state: params.state, + returnTo: params.returnTo, }; - const now = Date.now(); - const expiresAt = now + 24 * 60 * 60 * 1000; - - const session: AuthSession = { - user, - issuedAt: now, - expiresAt, - token: randomUUID(), - }; - - return session; + return useCase.execute(command); } async logout(): Promise { - // Intentionally does nothing; cookie deletion is handled by route handlers. - return; + const sessionPort = new CookieIdentitySessionAdapter(); + const useCase = new LogoutUseCase(sessionPort); + await useCase.execute(); } } \ No newline at end of file diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index f1a298ec8..9c97872a8 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -19,7 +19,11 @@ } ], "paths": { - "@/*": ["./*"] + "@/*": ["./*"], + "@gridpilot/identity/*": ["../../packages/identity/*"], + "@gridpilot/racing/*": ["../../packages/racing/*"], + "@gridpilot/social/*": ["../../packages/social/*"], + "@gridpilot/testing-support": ["../../packages/testing-support"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], diff --git a/packages/identity/application/dto/AuthCallbackCommandDTO.ts b/packages/identity/application/dto/AuthCallbackCommandDTO.ts new file mode 100644 index 000000000..df8fda1d5 --- /dev/null +++ b/packages/identity/application/dto/AuthCallbackCommandDTO.ts @@ -0,0 +1,8 @@ +import type { AuthProviderDTO } from './AuthProviderDTO'; + +export interface AuthCallbackCommandDTO { + provider: AuthProviderDTO; + code: string; + state: string; + returnTo?: string; +} \ No newline at end of file diff --git a/packages/identity/application/dto/AuthProviderDTO.ts b/packages/identity/application/dto/AuthProviderDTO.ts new file mode 100644 index 000000000..64080a8c2 --- /dev/null +++ b/packages/identity/application/dto/AuthProviderDTO.ts @@ -0,0 +1 @@ +export type AuthProviderDTO = 'IRACING_DEMO'; \ No newline at end of file diff --git a/packages/identity/application/dto/AuthSessionDTO.ts b/packages/identity/application/dto/AuthSessionDTO.ts new file mode 100644 index 000000000..71c8b0aac --- /dev/null +++ b/packages/identity/application/dto/AuthSessionDTO.ts @@ -0,0 +1,8 @@ +import type { AuthenticatedUserDTO } from './AuthenticatedUserDTO'; + +export interface AuthSessionDTO { + user: AuthenticatedUserDTO; + issuedAt: number; + expiresAt: number; + token: string; +} \ No newline at end of file diff --git a/packages/identity/application/dto/AuthenticatedUserDTO.ts b/packages/identity/application/dto/AuthenticatedUserDTO.ts new file mode 100644 index 000000000..708988575 --- /dev/null +++ b/packages/identity/application/dto/AuthenticatedUserDTO.ts @@ -0,0 +1,8 @@ +export interface AuthenticatedUserDTO { + id: string; + displayName: string; + email?: string; + iracingCustomerId?: string; + primaryDriverId?: string; + avatarUrl?: string; +} \ No newline at end of file diff --git a/packages/identity/application/dto/IracingAuthStateDTO.ts b/packages/identity/application/dto/IracingAuthStateDTO.ts new file mode 100644 index 000000000..f06d28e2a --- /dev/null +++ b/packages/identity/application/dto/IracingAuthStateDTO.ts @@ -0,0 +1,4 @@ +export interface IracingAuthStateDTO { + state: string; + returnTo?: string; +} \ No newline at end of file diff --git a/packages/identity/application/dto/StartAuthCommandDTO.ts b/packages/identity/application/dto/StartAuthCommandDTO.ts new file mode 100644 index 000000000..071adb8c9 --- /dev/null +++ b/packages/identity/application/dto/StartAuthCommandDTO.ts @@ -0,0 +1,6 @@ +import type { AuthProviderDTO } from './AuthProviderDTO'; + +export interface StartAuthCommandDTO { + provider: AuthProviderDTO; + returnTo?: string; +} \ No newline at end of file diff --git a/packages/identity/application/ports/IdentityProviderPort.ts b/packages/identity/application/ports/IdentityProviderPort.ts new file mode 100644 index 000000000..d4d955f8f --- /dev/null +++ b/packages/identity/application/ports/IdentityProviderPort.ts @@ -0,0 +1,8 @@ +import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO'; +import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO'; +import type { StartAuthCommandDTO } from '../dto/StartAuthCommandDTO'; + +export interface IdentityProviderPort { + startAuth(command: StartAuthCommandDTO): Promise<{ redirectUrl: string; state: string }>; + completeAuth(command: AuthCallbackCommandDTO): Promise; +} \ No newline at end of file diff --git a/packages/identity/application/ports/IdentitySessionPort.ts b/packages/identity/application/ports/IdentitySessionPort.ts new file mode 100644 index 000000000..9f0741712 --- /dev/null +++ b/packages/identity/application/ports/IdentitySessionPort.ts @@ -0,0 +1,8 @@ +import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO'; +import type { AuthSessionDTO } from '../dto/AuthSessionDTO'; + +export interface IdentitySessionPort { + getCurrentSession(): Promise; + createSession(user: AuthenticatedUserDTO): Promise; + clearSession(): Promise; +} \ No newline at end of file diff --git a/packages/identity/application/use-cases/GetCurrentUserSessionUseCase.ts b/packages/identity/application/use-cases/GetCurrentUserSessionUseCase.ts new file mode 100644 index 000000000..fb7480e0e --- /dev/null +++ b/packages/identity/application/use-cases/GetCurrentUserSessionUseCase.ts @@ -0,0 +1,14 @@ +import type { AuthSessionDTO } from '../dto/AuthSessionDTO'; +import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; + +export class GetCurrentUserSessionUseCase { + private readonly sessionPort: IdentitySessionPort; + + constructor(sessionPort: IdentitySessionPort) { + this.sessionPort = sessionPort; + } + + async execute(): Promise { + return this.sessionPort.getCurrentSession(); + } +} \ No newline at end of file diff --git a/packages/identity/application/use-cases/HandleAuthCallbackUseCase.ts b/packages/identity/application/use-cases/HandleAuthCallbackUseCase.ts new file mode 100644 index 000000000..c982aeaa1 --- /dev/null +++ b/packages/identity/application/use-cases/HandleAuthCallbackUseCase.ts @@ -0,0 +1,21 @@ +import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO'; +import type { AuthSessionDTO } from '../dto/AuthSessionDTO'; +import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO'; +import type { IdentityProviderPort } from '../ports/IdentityProviderPort'; +import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; + +export class HandleAuthCallbackUseCase { + private readonly provider: IdentityProviderPort; + private readonly sessionPort: IdentitySessionPort; + + constructor(provider: IdentityProviderPort, sessionPort: IdentitySessionPort) { + this.provider = provider; + this.sessionPort = sessionPort; + } + + async execute(command: AuthCallbackCommandDTO): Promise { + const user: AuthenticatedUserDTO = await this.provider.completeAuth(command); + const session = await this.sessionPort.createSession(user); + return session; + } +} \ No newline at end of file diff --git a/packages/identity/application/use-cases/LogoutUseCase.ts b/packages/identity/application/use-cases/LogoutUseCase.ts new file mode 100644 index 000000000..2be1932c2 --- /dev/null +++ b/packages/identity/application/use-cases/LogoutUseCase.ts @@ -0,0 +1,13 @@ +import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; + +export class LogoutUseCase { + private readonly sessionPort: IdentitySessionPort; + + constructor(sessionPort: IdentitySessionPort) { + this.sessionPort = sessionPort; + } + + async execute(): Promise { + await this.sessionPort.clearSession(); + } +} \ No newline at end of file diff --git a/packages/identity/application/use-cases/StartAuthUseCase.ts b/packages/identity/application/use-cases/StartAuthUseCase.ts new file mode 100644 index 000000000..d9832f497 --- /dev/null +++ b/packages/identity/application/use-cases/StartAuthUseCase.ts @@ -0,0 +1,14 @@ +import type { StartAuthCommandDTO } from '../dto/StartAuthCommandDTO'; +import type { IdentityProviderPort } from '../ports/IdentityProviderPort'; + +export class StartAuthUseCase { + private readonly provider: IdentityProviderPort; + + constructor(provider: IdentityProviderPort) { + this.provider = provider; + } + + async execute(command: StartAuthCommandDTO): Promise<{ redirectUrl: string; state: string }> { + return this.provider.startAuth(command); + } +} \ No newline at end of file diff --git a/packages/identity/domain/entities/User.ts b/packages/identity/domain/entities/User.ts new file mode 100644 index 000000000..1217a082a --- /dev/null +++ b/packages/identity/domain/entities/User.ts @@ -0,0 +1,73 @@ +import type { EmailValidationResult } from '../value-objects/EmailAddress'; +import { validateEmail } from '../value-objects/EmailAddress'; +import { UserId } from '../value-objects/UserId'; + +export interface UserProps { + id: UserId; + displayName: string; + email?: string; + iracingCustomerId?: string; + primaryDriverId?: string; + avatarUrl?: string; +} + +export class User { + private readonly id: UserId; + private displayName: string; + private email?: string; + private iracingCustomerId?: string; + private primaryDriverId?: string; + private avatarUrl?: string; + + private constructor(props: UserProps) { + if (!props.displayName || !props.displayName.trim()) { + throw new Error('User displayName cannot be empty'); + } + + this.id = props.id; + this.displayName = props.displayName.trim(); + this.email = props.email; + this.iracingCustomerId = props.iracingCustomerId; + this.primaryDriverId = props.primaryDriverId; + this.avatarUrl = props.avatarUrl; + } + + public static create(props: UserProps): User { + if (props.email) { + const result: EmailValidationResult = validateEmail(props.email); + if (!result.success) { + throw new Error(result.error); + } + return new User({ + ...props, + email: result.email, + }); + } + + return new User(props); + } + + public getId(): UserId { + return this.id; + } + + public getDisplayName(): string { + return this.displayName; + } + + public getEmail(): string | undefined { + return this.email; + } + + public getIracingCustomerId(): string | undefined { + return this.iracingCustomerId; + } + + public getPrimaryDriverId(): string | undefined { + return this.primaryDriverId; + } + + public getAvatarUrl(): string | undefined { + return this.avatarUrl; + } +} \ No newline at end of file diff --git a/packages/identity/domain/value-objects/UserId.ts b/packages/identity/domain/value-objects/UserId.ts new file mode 100644 index 000000000..0fb2bcadd --- /dev/null +++ b/packages/identity/domain/value-objects/UserId.ts @@ -0,0 +1,22 @@ +export class UserId { + private readonly value: string; + + private constructor(value: string) { + if (!value || !value.trim()) { + throw new Error('UserId cannot be empty'); + } + this.value = value; + } + + public static fromString(value: string): UserId { + return new UserId(value); + } + + public toString(): string { + return this.value; + } + + public equals(other: UserId): boolean { + return this.value === other.value; + } +} \ No newline at end of file diff --git a/packages/identity/index.ts b/packages/identity/index.ts index 4420c5f58..0cf1c0711 100644 --- a/packages/identity/index.ts +++ b/packages/identity/index.ts @@ -1 +1,15 @@ -export * from './domain/value-objects/EmailAddress'; \ No newline at end of file +export * from './domain/value-objects/EmailAddress'; +export * from './domain/value-objects/UserId'; +export * from './domain/entities/User'; + +export * from './application/dto/AuthenticatedUserDTO'; +export * from './application/dto/AuthSessionDTO'; +export * from './application/dto/AuthCallbackCommandDTO'; +export * from './application/dto/StartAuthCommandDTO'; +export * from './application/dto/AuthProviderDTO'; +export * from './application/dto/IracingAuthStateDTO'; + +export * from './application/use-cases/StartAuthUseCase'; +export * from './application/use-cases/HandleAuthCallbackUseCase'; +export * from './application/use-cases/GetCurrentUserSessionUseCase'; +export * from './application/use-cases/LogoutUseCase'; \ No newline at end of file diff --git a/packages/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter.ts b/packages/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter.ts new file mode 100644 index 000000000..e07f42d53 --- /dev/null +++ b/packages/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter.ts @@ -0,0 +1,50 @@ +import { randomUUID } from 'crypto'; +import { createStaticRacingSeed } from '../../../testing-support'; +import type { IdentityProviderPort } from '../../application/ports/IdentityProviderPort'; +import type { StartAuthCommandDTO } from '../../application/dto/StartAuthCommandDTO'; +import type { AuthCallbackCommandDTO } from '../../application/dto/AuthCallbackCommandDTO'; +import type { AuthenticatedUserDTO } from '../../application/dto/AuthenticatedUserDTO'; + +export class IracingDemoIdentityProviderAdapter implements IdentityProviderPort { + private readonly seedDriverId: string; + + constructor() { + const seed = createStaticRacingSeed(42); + this.seedDriverId = seed.drivers[0]?.id ?? 'driver-1'; + } + + async startAuth(command: StartAuthCommandDTO): Promise<{ redirectUrl: string; state: string }> { + const state = randomUUID(); + + const params = new URLSearchParams(); + params.set('code', 'dummy-code'); + params.set('state', state); + if (command.returnTo) { + params.set('returnTo', command.returnTo); + } + + return { + redirectUrl: `/auth/iracing/callback?${params.toString()}`, + state, + }; + } + + async completeAuth(command: AuthCallbackCommandDTO): Promise { + if (!command.code) { + throw new Error('Missing auth code'); + } + if (!command.state) { + throw new Error('Missing auth state'); + } + + const user: AuthenticatedUserDTO = { + id: 'demo-user', + displayName: 'GridPilot Demo Driver', + iracingCustomerId: '000000', + primaryDriverId: this.seedDriverId, + avatarUrl: `/api/avatar/${this.seedDriverId}`, + }; + + return user; + } +} \ No newline at end of file diff --git a/packages/identity/infrastructure/session/CookieIdentitySessionAdapter.ts b/packages/identity/infrastructure/session/CookieIdentitySessionAdapter.ts new file mode 100644 index 000000000..b97982422 --- /dev/null +++ b/packages/identity/infrastructure/session/CookieIdentitySessionAdapter.ts @@ -0,0 +1,59 @@ +import { cookies } from 'next/headers'; +import { randomUUID } from 'crypto'; +import type { AuthenticatedUserDTO } from '../../application/dto/AuthenticatedUserDTO'; +import type { AuthSessionDTO } from '../../application/dto/AuthSessionDTO'; +import type { IdentitySessionPort } from '../../application/ports/IdentitySessionPort'; + +const SESSION_COOKIE = 'gp_demo_session'; + +function parseCookieValue(raw: string | undefined): AuthSessionDTO | null { + if (!raw) return null; + try { + const parsed = JSON.parse(raw) as AuthSessionDTO; + if (!parsed.expiresAt || Date.now() > parsed.expiresAt) { + return null; + } + return parsed; + } catch { + return null; + } +} + +function serializeSession(session: AuthSessionDTO): string { + return JSON.stringify(session); +} + +export class CookieIdentitySessionAdapter implements IdentitySessionPort { + async getCurrentSession(): Promise { + const store = await cookies(); + const raw = store.get(SESSION_COOKIE)?.value; + return parseCookieValue(raw); + } + + async createSession(user: AuthenticatedUserDTO): Promise { + const now = Date.now(); + const expiresAt = now + 24 * 60 * 60 * 1000; + + const session: AuthSessionDTO = { + user, + issuedAt: now, + expiresAt, + token: randomUUID(), + }; + + const store = await cookies(); + store.set(SESSION_COOKIE, serializeSession(session), { + httpOnly: true, + sameSite: 'lax', + path: '/', + secure: process.env.NODE_ENV === 'production', + }); + + return session; + } + + async clearSession(): Promise { + const store = await cookies(); + store.delete(SESSION_COOKIE); + } +} \ No newline at end of file diff --git a/packages/identity/package.json b/packages/identity/package.json index 60a481b3c..c855a5144 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -5,7 +5,8 @@ "types": "./index.ts", "type": "module", "exports": { - "./domain/*": "./domain/*" + "./domain/*": "./domain/*", + "./application/*": "./application/*" }, "dependencies": { "zod": "^3.25.76" diff --git a/packages/identity/tsconfig.json b/packages/identity/tsconfig.json index 5558a6a0b..b7aa6ee1e 100644 --- a/packages/identity/tsconfig.json +++ b/packages/identity/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "rootDir": ".", "outDir": "dist", "declaration": true, "declarationMap": false diff --git a/tsconfig.json b/tsconfig.json index a76a08ab4..eb27c3583 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,8 @@ "packages/*": ["packages/*"], "apps/*": ["apps/*"], "@gridpilot/shared-result": ["packages/shared/result/Result.ts"], - "@gridpilot/automation/*": ["packages/automation/*"] + "@gridpilot/automation/*": ["packages/automation/*"], + "@gridpilot/testing-support": ["packages/testing-support/index.ts"] }, "types": ["vitest/globals", "node"], "jsx": "react-jsx"