From 327f1a5d46f3253551599f3cf9787ea8e7b0493a Mon Sep 17 00:00:00 2001 From: Hossein Pourdavar Date: Fri, 2 Oct 2026 09:13:43 +0330 Subject: [PATCH 1/3] docs: rebuild code-derived documentation --- PACKAGE_GENERATOR.md | 173 -- README.md | 538 +----- docs/architecture.md | 34 + docs/authentication.md | 38 + docs/ddd.md | 16 + docs/decision-engine.md | 117 ++ docs/development.md | 54 + docs/nestjs-integrations.md | 53 + docs/packages.md | 23 + .../specs/2026-08-08-ddd-primitives-design.md | 55 - packages/authentication/core/Architecture.md | 164 -- packages/authentication/core/Definition.md | 125 -- packages/authentication/core/Develop.md | 0 packages/authentication/core/FUTURE.md | 1495 ----------------- packages/authentication/core/GAP_ANALYSIS.md | 47 - .../core/IMPLEMENTATION_PLAN.md | 51 - packages/authentication/core/ONBOARDING.md | 57 - packages/authentication/core/README.md | 68 +- packages/authentication/core/ROADMAP.md | 38 - packages/authentication/core/RULES.md | 89 - .../authentication/core/TASK_BREAKDOWN.md | 42 - .../core/src/domain/errors/README.md | 118 -- .../core/src/domain/value-objects/README.md | 101 -- packages/authentication/methods/otp/README.md | 89 +- .../methods/passwordless/DOCS.md | 5 - .../passwordless/High-level-Architrcture.md | 45 - .../methods/passwordless/README.md | 513 +----- packages/ddd/README.md | 850 +--------- packages/ddd/docs/DDD-GUIDE.md | 102 -- packages/ddd/docs/MIGRATION.md | 48 - packages/decision-engine/README.md | 158 +- packages/eslint-config/README.md | 14 + packages/ioredis/README.md | 226 +-- packages/libs/README.md | 27 +- .../nest/cookie-middleware-module/README.md | 219 +-- .../nest/cors-middleware-module/README.md | 175 +- .../nest/helmet-middleware-module/README.md | 172 +- .../no-favicon-middleware-module/README.md | 125 +- .../response-time-middleware-module/README.md | 174 +- packages/pg-slonik/README.md | 574 +------ packages/pg-slonik/docs/API.md | 194 --- packages/pg-slonik/example/README.md | 54 +- packages/typescript-config/README.md | 8 + plan/feature-decision-engine-1.md | 161 -- plan/refactor-ddd-primitives-1.md | 167 -- 45 files changed, 497 insertions(+), 7099 deletions(-) delete mode 100644 PACKAGE_GENERATOR.md create mode 100644 docs/architecture.md create mode 100644 docs/authentication.md create mode 100644 docs/ddd.md create mode 100644 docs/decision-engine.md create mode 100644 docs/development.md create mode 100644 docs/nestjs-integrations.md create mode 100644 docs/packages.md delete mode 100644 docs/superpowers/specs/2026-08-08-ddd-primitives-design.md delete mode 100644 packages/authentication/core/Architecture.md delete mode 100644 packages/authentication/core/Definition.md delete mode 100644 packages/authentication/core/Develop.md delete mode 100644 packages/authentication/core/FUTURE.md delete mode 100644 packages/authentication/core/GAP_ANALYSIS.md delete mode 100644 packages/authentication/core/IMPLEMENTATION_PLAN.md delete mode 100644 packages/authentication/core/ONBOARDING.md delete mode 100644 packages/authentication/core/ROADMAP.md delete mode 100644 packages/authentication/core/RULES.md delete mode 100644 packages/authentication/core/TASK_BREAKDOWN.md delete mode 100644 packages/authentication/core/src/domain/errors/README.md delete mode 100644 packages/authentication/core/src/domain/value-objects/README.md delete mode 100644 packages/authentication/methods/passwordless/DOCS.md delete mode 100644 packages/authentication/methods/passwordless/High-level-Architrcture.md delete mode 100644 packages/ddd/docs/DDD-GUIDE.md delete mode 100644 packages/ddd/docs/MIGRATION.md create mode 100644 packages/eslint-config/README.md delete mode 100644 packages/pg-slonik/docs/API.md create mode 100644 packages/typescript-config/README.md delete mode 100644 plan/feature-decision-engine-1.md delete mode 100644 plan/refactor-ddd-primitives-1.md diff --git a/PACKAGE_GENERATOR.md b/PACKAGE_GENERATOR.md deleted file mode 100644 index 63b17b0..0000000 --- a/PACKAGE_GENERATOR.md +++ /dev/null @@ -1,173 +0,0 @@ -# Package Generator Guide - -Quick and easy way to scaffold new packages in the Rineex monorepo following the -established structure and configuration standards. - -## Usage - -### Option 1: Using npm script (Recommended) - -```bash -pnpm g:pkg [description] -``` - -### Option 2: Using Node.js directly - -```bash -node scripts/generate-package.mjs [description] -``` - -### Option 3: Using shell script - -```bash -./scripts/generate-package.sh [description] -``` - -## Examples - -### Create a basic package - -```bash -pnpm g:pkg validation -``` - -### Create a package with description - -```bash -pnpm g:pkg validation "Input validation utilities" -``` - -### Create a more complex package - -```bash -pnpm g:pkg cache-manager "Redis and in-memory cache management" -``` - -## What Gets Generated - -The generator creates a fully configured package with: - -- **package.json** - Complete configuration with: - - Proper npm scripts (build, test, lint, check-types) - - ESM/CJS dual exports - - TypeScript definitions - - Workspace dependencies - - Publishable metadata - -- **TypeScript Configuration** - - tsconfig.json - - tsconfig.build.json (for builds) - - Path aliases (@/\*) - -- **Build Configuration** - - tsup.config.ts (for bundling) - - vitest.config.ts (for testing) - - eslint.config.mjs (for linting) - -- **Initial Files** - - src/index.ts (empty export) - - README.md (with installation and development instructions) - -## Package Naming Rules - -Package names must: - -- Use lowercase letters, numbers, and hyphens -- Start and end with a letter or number -- Be unique in the packages directory - -Examples: ✅ `auth-core`, `cache-manager`, `validation` ❌ `Auth-Core`, -`cache_manager` - -## Next Steps After Generation - -1. **Install dependencies** - - ```bash - pnpm install - ``` - -2. **Develop your package** - - Add code to `packages//src/` - - Export types/functions from `src/index.ts` - - Create test files with `.spec.ts` suffix - -3. **Common development commands** - - ```bash - # For a specific package - pnpm -F @rineex/ build - pnpm -F @rineex/ test - pnpm -F @rineex/ lint - pnpm -F @rineex/ check-types - - # Or from the package directory - cd packages/ - pnpm build - ``` - -4. **Publishing** - - Add a changeset: `pnpm changeset` - - Version and publish: `pnpm changeset:publish` - -## Configuration Details - -All generated packages use: - -- **TypeScript**: v5.9.3 -- **Build Tool**: tsup (for ESM/CJS bundling) -- **Test Runner**: vitest -- **Linter**: ESLint with @rineex/eslint-config -- **Node.js**: ES6 target, CommonJS module format -- **License**: Apache-2.0 - -## Package Structure - -``` -packages// -├── src/ -│ └── index.ts -├── package.json -├── tsconfig.json -├── tsconfig.build.json -├── tsup.config.ts -├── vitest.config.ts -├── eslint.config.mjs -├── README.md -└── dist/ (generated after build) -``` - -## Tips - -- Use descriptive package names that indicate their purpose -- Keep packages focused on a single responsibility -- Reference similar packages (like `ddd` or `auth-core`) as examples -- Test your package locally before publishing - -## Troubleshooting - -**Package already exists** - -``` -Error: Package directory already exists -``` - -Choose a different package name or remove the existing directory first. - -**Invalid package name** - -``` -Error: Invalid package name. Use lowercase letters, numbers, and hyphens only. -``` - -Ensure your package name follows the naming rules above. - -**Build fails after generation** - -```bash -# Run pnpm install in the workspace root -cd /path/to/core -pnpm install -``` - -Then try building again: `pnpm -F @rineex/ build` diff --git a/README.md b/README.md index 3edc495..8994bd1 100644 --- a/README.md +++ b/README.md @@ -1,528 +1,40 @@ # Rineex Core -> A monorepo containing essential, production-grade packages for building -> scalable and maintainable TypeScript/Node.js applications. +Rineex Core is a pnpm workspace of framework-agnostic domain primitives and +reusable NestJS integrations. Published packages are built as CommonJS, ESM, and +TypeScript declarations from their `src/index.ts` entry points. -> ⚠️ **Note**: This package is currently under heavy development. APIs may -> change without notice. - -[![monorepo](https://img.shields.io/badge/monorepo-turbo-blue.svg)](https://turbo.build/) -[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue.svg)](https://www.typescriptlang.org/) - -## Overview - -Rineex Core is a collection of carefully crafted, battle-tested packages -designed following Domain-Driven Design (DDD) principles and industry best -practices. Each package is independently versioned and published to npm, -enabling teams to adopt only what they need. - -## Packages - -### [`@rineex/ddd`](./packages/ddd) – Domain-Driven Design Framework - -[![npm version](https://img.shields.io/npm/v/@rineex/ddd)](https://www.npmjs.com/package/@rineex/ddd) -[![npm downloads](https://img.shields.io/npm/dm/@rineex/ddd)](https://www.npmjs.com/package/@rineex/ddd) - -A comprehensive, production-grade TypeScript library providing battle-tested -abstractions for implementing Domain-Driven Design patterns. Build maintainable, -scalable applications with type-safe entities, aggregates, value objects, and -domain events. - -#### Quick Summary - -- **Value Objects**: Immutable objects distinguished by value, not identity -- **Entities**: Objects with unique identity and lifecycle -- **Aggregate Roots**: Entry points to aggregates with invariant enforcement and - event support -- **Domain Events**: Immutable records of significant domain occurrences -- **Application Services**: Orchestrators for use cases and commands -- **Zero Dependencies**: Lightweight with only peer dependencies - -#### Key Features - -✨ **Type-Safe Abstractions** - Fully typed base classes for all DDD building -blocks -🔒 **Immutability by Default** - Value objects and entities are frozen to -prevent mutations -📡 **Domain Events Support** - First-class support for event sourcing and -event-driven architectures -✅ **Built-in Validation** - Enforce domain rules at aggregate boundaries -⚡ **Zero Dependencies** - Minimal bundle footprint for maximum flexibility -🏢 **Production Ready** - Used in high-performance systems at scale - -#### Quick Start - -```bash -npm install @rineex/ddd -``` - -```typescript -import { - AggregateRoot, - ValueObject, - AggregateId, - DomainEvent, -} from '@rineex/ddd'; - -// Define a Value Object -class Email extends ValueObject { - public static create(value: string) { - return new Email(value); - } - protected validate(props: string): void { - if (!props.includes('@')) throw new Error('Invalid email'); - } -} - -// Create an Aggregate Root -interface UserProps { - email: Email; - isActive: boolean; -} - -class User extends AggregateRoot { - get email(): Email { - return this.props.email; - } - protected validate(): void { - if (!this.email) throw new Error('Email is required'); - } -} - -// Use it -const user = new User({ - id: AggregateId.create(), - createdAt: new Date(), - props: { email: Email.create('user@example.com'), isActive: true }, -}); -``` - -#### Comprehensive Documentation - -For in-depth guidance, examples, and best practices, see the -[**@rineex/ddd Documentation**](./packages/ddd/README.md): - -- 📚 [Core Concepts Guide](./packages/ddd/README.md#core-concepts) - Value - Objects, Entities, Aggregates, Events -- 🔧 [Complete API Reference](./packages/ddd/README.md#api-reference) - All - classes, interfaces, and methods -- 💡 [Examples](./packages/ddd/README.md#examples) - Order aggregate and - integration patterns -- ✅ [Best Practices](./packages/ddd/README.md#best-practices) - DDD patterns - and principles -- 🚨 [Error Handling](./packages/ddd/README.md#error-handling) - Registry-backed - domain errors and Result -- 🤝 [Contributing Guide](./packages/ddd/README.md#contributing) - Development - setup and guidelines - -#### Included Value Objects - -- `AggregateId` - Unique identifier for aggregates -- `Email` - Email address validation -- `IPAddress` - IPv4/IPv6 validation - ---- - -### [`@rineex/ioredis`](./packages/ioredis) – Redis Adapter for NestJS - -[![npm version](https://img.shields.io/npm/v/@rineex/ioredis)](https://www.npmjs.com/package/@rineex/ioredis) - -NestJS integration for [`ioredis`](https://www.npmjs.com/package/ioredis), -providing Redis client management with dependency injection, lifecycle -management, and health check integration. - -#### Key Features - -- **Multiple Connections** - Support for single instance or cluster - configurations with named connections -- **Lifecycle Management** - Automatic connection health checks on bootstrap and - graceful shutdown -- **Health Check Integration** - Built-in Terminus health indicator for - monitoring -- **Dependency Injection** - Seamless NestJS DI integration with - `@InjectRedis()` decorator - -#### Quick Start - -```bash -npm install @rineex/ioredis -``` - -```typescript -import { Module, Injectable } from '@nestjs/common'; -import { InjectRedis, RedisModule } from '@rineex/ioredis'; -import type Redis from 'ioredis'; - -@Module({ - imports: [ - RedisModule.register({ - type: 'single', - url: process.env.REDIS_URL ?? 'redis://localhost:6379', - }), - ], -}) -export class AppModule {} - -@Injectable() -export class CacheService { - constructor(@InjectRedis() private readonly redis: Redis) {} - - async setValue(key: string, value: string) { - await this.redis.set(key, value); - } -} -``` - -For complete documentation, see the -[@rineex/ioredis README](./packages/ioredis/README.md). - ---- - -### [`@rineex/pg-slonik`](./packages/pg-slonik) – PostgreSQL Adapter using Slonik - -[![npm version](https://img.shields.io/npm/v/@rineex/pg-slonik)](https://www.npmjs.com/package/@rineex/pg-slonik) - -NestJS module wrapper for [Slonik](https://github.com/gajus/slonik), providing -type-safe PostgreSQL database access with dependency injection, automatic retry -logic, and graceful shutdown handling. - -#### Key Features - -- **Type-Safe Database Access** - Full TypeScript support with Slonik's type - system -- **Multiple Connections** - Support for multiple named database connections -- **Automatic Retry Logic** - Built-in connection retry with configurable - attempts and delays -- **SQL Safety** - Built-in SQL injection protection via Slonik's tagged - template literals -- **Transaction Support** - First-class transaction handling - -#### Quick Start +## Quick start ```bash -npm install @rineex/pg-slonik -``` - -```typescript -import { Module, Injectable } from '@nestjs/common'; -import { InjectPool, SlonikModule } from '@rineex/pg-slonik'; -import type { DatabasePool } from 'slonik'; - -@Module({ - imports: [ - SlonikModule.register({ - connections: [ - { - name: 'DEFAULT', - dsn: process.env.DATABASE_URL!, - }, - ], - }), - ], -}) -export class AppModule {} - -@Injectable() -export class UserService { - constructor(@InjectPool() private readonly pool: DatabasePool) {} - - async findUser(id: string) { - const result = await this.pool.query( - this.pool.sql`SELECT * FROM users WHERE id = ${id}`, - ); - return result.rows[0]; - } -} -``` - -For complete documentation, see the -[@rineex/pg-slonik README](./packages/pg-slonik/README.md). - ---- - -### [`@rineex/auth-core`](./packages/authentication/core) – Authentication Core - -[![npm version](https://img.shields.io/npm/v/@rineex/auth-core)](https://www.npmjs.com/package/@rineex/auth-core) - -Core authentication package providing foundational abstractions for building -authentication systems. Designed following DDD principles with support for -multiple authentication methods. - -#### Key Features - -- **Domain-Driven Design** - Built on `@rineex/ddd` for maintainable - authentication logic -- **Extensible Architecture** - OTP and passwordless method packages; - `AuthMethodPort` plugin model -- **Type-Safe** - Full TypeScript support with strict typing -- **Framework Agnostic** - Core domain logic independent of framework specifics - -See [Architecture.md](./packages/authentication/core/Architecture.md) for what -is implemented vs in-tree. - -For complete documentation, see the -[@rineex/auth-core README](./packages/authentication/core/README.md). - ---- - -### [`@rineex/libs`](./packages/libs) – Utility Libraries - -[![npm version](https://img.shields.io/npm/v/@rineex/libs)](https://www.npmjs.com/package/@rineex/libs) - -Shared utility libraries and helper functions for Rineex core modules. - ---- - -### NestJS Middleware Modules - -Production-ready NestJS middleware modules for common web application needs: - -#### [`@rineex/helmet-mw-module`](./packages/nest/helmet-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/helmet-mw-module)](https://www.npmjs.com/package/@rineex/helmet-mw-module) - -Helmet middleware module for securing HTTP headers in NestJS applications. - -#### [`@rineex/cors-mw-module`](./packages/nest/cors-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/cors-mw-module)](https://www.npmjs.com/package/@rineex/cors-mw-module) - -CORS middleware module for handling cross-origin resource sharing in NestJS -applications. - -#### [`@rineex/cookie-parser-mw-module`](./packages/nest/cookie-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/cookie-parser-mw-module)](https://www.npmjs.com/package/@rineex/cookie-parser-mw-module) - -Cookie parser middleware module for parsing cookies in NestJS applications. - -#### [`@rineex/response-time-mw-module`](./packages/nest/response-time-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/response-time-mw-module)](https://www.npmjs.com/package/@rineex/response-time-mw-module) - -Response time middleware module for measuring and reporting response times in -NestJS applications. - -#### [`@rineex/favicon-ignore-mw-module`](./packages/nest/no-favicon-middleware-module) - -[![npm version](https://img.shields.io/npm/v/@rineex/favicon-ignore-mw-module)](https://www.npmjs.com/package/@rineex/favicon-ignore-mw-module) - -Favicon ignore middleware module to prevent favicon requests from cluttering -logs in NestJS applications. - ---- - -### Shared Configuration Packages - -#### [`@rineex/eslint-config`](./packages/eslint-config) - -Shared ESLint configuration for Rineex packages. Provides consistent linting -rules across the monorepo. - -**Exports:** - -- `@rineex/eslint-config/base` - Base ESLint configuration -- `@rineex/eslint-config/next-js` - Next.js specific configuration -- `@rineex/eslint-config/react-internal` - React internal configuration -- `@rineex/eslint-config/db` - Database/SQL linting configuration - -#### [`@rineex/typescript-config`](./packages/typescript-config) - -Shared TypeScript configuration for Rineex packages. Provides consistent -TypeScript compiler settings. - -**Exports:** - -- `@rineex/typescript-config/base` - Base TypeScript configuration -- `@rineex/typescript-config/react-library` - React library configuration - ---- - -## Project Structure - -``` -. -├── packages/ -│ ├── ddd/ # Domain-Driven Design framework -│ │ ├── src/ -│ │ │ ├── domain/ # Core DDD building blocks -│ │ │ ├── application/ # Application service ports -│ │ │ ├── gateway/ # HTTP constants and utilities -│ │ │ └── utils/ # Helper utilities -│ │ ├── README.md # Complete documentation -│ │ └── package.json -│ ├── ioredis/ # Redis adapter for NestJS -│ ├── pg-slonik/ # PostgreSQL adapter using Slonik -│ ├── libs/ # Utility libraries -│ ├── authentication/ # Authentication packages -│ │ ├── core/ # @rineex/auth-core -│ │ └── methods/ # OTP, passwordless method packages -│ ├── nest/ # NestJS middleware modules -│ │ ├── helmet-middleware-module/ -│ │ ├── cors-middleware-module/ -│ │ ├── cookie-middleware-module/ -│ │ ├── response-time-middleware-module/ -│ │ └── no-favicon-middleware-module/ -│ ├── eslint-config/ # Shared ESLint configuration -│ └── typescript-config/ # Shared TypeScript configuration -├── scripts/ # Build and utility scripts -├── turbo.json # Turborepo configuration -├── pnpm-workspace.yaml # PNPM workspace configuration -└── README.md -``` - -## Development - -### Prerequisites - -- **Node.js**: 18.0 or higher (Volta pins Node 24 in this repo) -- **pnpm**: 10.0 or higher (workspace uses pnpm 10.22) -- **TypeScript**: 5.9 or higher - -### Getting Started - -```bash -# Install dependencies +corepack enable pnpm install - -# Build all packages pnpm build - -# Run tests pnpm test - -# Run linter -pnpm lint - -# Check types -pnpm check-types ``` -### Workspace Scripts - -The monorepo uses [Turborepo](https://turbo.build/) for efficient task execution -across packages. - -```bash -# Run command in all packages -pnpm turbo run build - -# Run command in specific package -pnpm --filter @rineex/ddd build - -# Watch mode for development -pnpm turbo run dev --parallel -``` - -## Publishing - -Packages are published automatically through GitHub Actions when changes are -merged to the main branch. Version management is handled via -[Changesets](https://github.com/changesets/changesets). - -### Creating a Changeset - -```bash -pnpm changeset -``` - -Follow the prompts to document your changes. This will create a changeset file -that will be used to automatically bump versions and generate changelogs. - -## Architecture Principles - -This monorepo follows these key principles: - -1. **Domain-Driven Design** - Clear separation of domain logic from - infrastructure -2. **Type Safety** - Strict TypeScript configuration for compile-time safety -3. **Immutability** - Default-immutable data structures to prevent bugs -4. **Composition** - Small, focused packages that can be used independently -5. **Zero Dependencies** - Minimal external dependencies for reliability -6. **Testing** - Comprehensive test coverage with Vitest - -## Contributing - -We welcome contributions! Please follow these guidelines when contributing: - -### Quick Start for Contributors - -```bash -# Clone and setup -git clone https://github.com/rineex/core.git -cd core -pnpm install - -# Create a feature branch -git checkout -b feature/your-feature - -# Make changes and test -pnpm test -pnpm lint - -# Push and open a PR -git push origin feature/your-feature -``` - -### Development Guidelines - -- **Code Style**: Follow the ESLint configuration provided by - `@rineex/eslint-config` -- **Type Safety**: Ensure all code passes TypeScript type checking - (`pnpm check-types`) -- **Testing**: Write tests for new functionality and ensure all tests pass -- **Documentation**: Update relevant README files and add JSDoc comments for - public APIs -- **Changesets**: Create a changeset for any package changes using - `pnpm changeset` - -### Package Generator - -Use the package generator to scaffold new packages following the established -structure: - -```bash -pnpm g:pkg [description] -``` - -For more details, see [PACKAGE_GENERATOR.md](./PACKAGE_GENERATOR.md). - -## Documentation Map - -| Package / area | Primary docs | -| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Monorepo | [README.md](./README.md), [PACKAGE_GENERATOR.md](./PACKAGE_GENERATOR.md) | -| `@rineex/ddd` | [packages/ddd/README.md](./packages/ddd/README.md) | -| `@rineex/auth-core` | [packages/authentication/core/README.md](./packages/authentication/core/README.md), [Architecture.md](./packages/authentication/core/Architecture.md) | -| Auth methods | [passwordless README](./packages/authentication/methods/passwordless/README.md), [OTP README](./packages/authentication/methods/otp/README.md) | -| `@rineex/pg-slonik` | [README](./packages/pg-slonik/README.md), [API](./packages/pg-slonik/docs/API.md) | -| `@rineex/ioredis` | [README](./packages/ioredis/README.md) | -| Nest middleware | `packages/nest/*/README.md` | -| Domain sub-docs | [auth errors](./packages/authentication/core/src/domain/errors/README.md), [auth VOs](./packages/authentication/core/src/domain/value-objects/README.md) | - -## License - -This project is licensed under the Apache License 2.0 - see the -[LICENSE](LICENSE) file for details. - -## Support +The workspace pins pnpm 10.22.0 and specifies Node 24.12.0 through Volta. +`@rineex/pg-slonik` supports Node 18 or later; use the root toolchain while +developing this repository. -- 📖 **Documentation** - See individual package READMEs -- 🐛 **Issues** - Report bugs on - [GitHub Issues](https://github.com/rineex/core/issues) -- 💬 **Discussions** - Ask questions on - [GitHub Discussions](https://github.com/rineex/core/discussions) +## Package areas -## Related Resources +| Area | Packages | +| ---------------------- | --------------------------------------------------------- | +| Domain building blocks | `@rineex/ddd`, `@rineex/decision-engine` | +| Authentication | `@rineex/auth-core`, OTP and passwordless method packages | +| NestJS integrations | Redis, Slonik/PostgreSQL, and HTTP middleware modules | +| Tooling | shared ESLint and TypeScript configurations | -- [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://www.domainlanguage.com/ddd/) - by Eric Evans -- [Implementing Domain-Driven Design](https://vaughnvernon.com/books/) by Vaughn - Vernon -- [Turborepo Documentation](https://turbo.build/repo/docs) -- [TypeScript Handbook](https://www.typescriptlang.org/docs/) +## Documentation ---- +- [Development workflow](docs/development.md) +- [Architecture](docs/architecture.md) +- [Package catalog](docs/packages.md) +- [Decision engine](docs/decision-engine.md) +- [Authentication](docs/authentication.md) +- [NestJS integrations](docs/nestjs-integrations.md) +- [DDD primitives](docs/ddd.md) -**Made with ❤️ by the Rineex Team** +Every package has a focused README next to its source. Package release history +remains in its `CHANGELOG.md`. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..038e591 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,34 @@ +# Architecture + +Rineex Core is a library monorepo, not an application. The root package +orchestrates workspace-wide build, test, lint, formatting, and release commands. +Each package owns its source, build configuration, tests, and public entry +point. + +## Dependency direction + +`@rineex/ddd` supplies general domain abstractions. Authentication packages +build on it. The decision engine is standalone. NestJS integration packages +adapt third-party infrastructure to Nest's dependency-injection lifecycle and +should remain at the edge of an application. + +```text +@rineex/ddd ──> @rineex/auth-core ──> authentication method packages + +application ──> @rineex/decision-engine +application ──> NestJS integration packages ──> Redis / Slonik / Express middleware +``` + +## Public API rule + +Treat only a package's `src/index.ts` exports as its supported API. Source files +not exported there are implementation details, even when they are visible in the +repository. This protects consumers from deep-import breakage and keeps +documentation aligned with published artifacts. + +## Build artifacts + +Packages declare `main`, `module`, and `types` paths under `dist/`; tsup +produces those artifacts. Configuration packages are private workspace +dependencies and expose configuration files rather than a runtime library entry +point. diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..8319f92 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,38 @@ +# Authentication + +Authentication packages separate core domain contracts from method +implementations. `@rineex/auth-core` is the supported public foundation; adapt +its repository, session, event-publisher, log, and authentication-method ports +in the application layer. + +## Core public surface + +`@rineex/auth-core` exports identity aggregates/entities/value objects and +lifecycle events, policy contracts and `AuthPolicyEngine`, inbound/outbound +ports, and authentication context/factor/method/policy/provider/risk types. It +does not export its application services, OAuth implementation, MFA +implementation, or infrastructure code from the package root. + +This means consumers should compose the public domain contracts rather than +importing internal service paths. + +## OTP method + +`OtpAuthMethod` implements the core authentication-method port. Give it an +`OtpChannelPort` and an OTP generator. The channel is responsible for delivery +and verification; it can use SMS, email, push, voice, or another transport. + +```ts +const method = new OtpAuthMethod(channel, () => OtpCode.create('123456')); +``` + +The `start` input must expose an `IdentityId` at `ctx.identityId` or +`ctx.metadata.identityId`. The `verify` payload must contain an `IdentityId` and +a string `code`. + +## Passwordless method status + +The passwordless package contains challenge aggregate, service, registry, port, +and channel source code, but its package entry point currently exports nothing. +It is therefore not a supported consumable API yet. Avoid deep imports until its +`src/index.ts` intentionally exports a public contract. diff --git a/docs/ddd.md b/docs/ddd.md new file mode 100644 index 0000000..2613492 --- /dev/null +++ b/docs/ddd.md @@ -0,0 +1,16 @@ +# DDD primitives + +`@rineex/ddd` supplies reusable TypeScript building blocks rather than a +persistence or application framework. + +| Group | Exports | +| -------------------------- | -------------------------------------------------------------------------------------------- | +| Domain modeling | `ValueObject`, `PrimitiveValueObject`, `Entity`, `AggregateRoot`, `DomainEvent` | +| Value objects | `AggregateId`, `DomainID`, `Email`, `IPAddress`, `Timestamp`, `Url`, `UserAgent` | +| Result and errors | `Result`, `DomainError`, error registry, internal/invalid-state/invalid-value/timeout errors | +| Types and utilities | entity and mapper types, deep-primitive type, `deepFreeze` | +| Application/infrastructure | application-service and logger ports, `BaseMapper`, HTTP status constants | + +Value objects are the right boundary for validation and equality. Entities and +aggregates model identity-bearing state. Use `Result` when callers should handle +success or a typed error explicitly instead of relying on exceptions. diff --git a/docs/decision-engine.md b/docs/decision-engine.md new file mode 100644 index 0000000..68a87bd --- /dev/null +++ b/docs/decision-engine.md @@ -0,0 +1,117 @@ +# Decision engine + +`@rineex/decision-engine` is a deterministic, synchronous pipeline for choosing +candidates. It has no persistence, I/O, logging, or implicit event dispatch; +callers own those concerns. + +## Execution model + +1. Validate the definition and request. +2. Evaluate every hard constraint. +3. Extract features for eligible candidates. +4. Normalize feature values, score candidates, and rank them. +5. Select the final ranked candidates. +6. Return frozen evaluations, a result, and `candidate.rejected` / + `decision.completed` events. + +Constraints explain ineligibility. Features are numeric preferences with +`FeatureObjective.MAXIMIZE` or `FeatureObjective.MINIMIZE`. A policy provides +the weights or other business values used by strategies. + +## Example + +```ts +import { + DecisionEngine, + DefaultDecisionValidator, + DescendingScoreRankingStrategy, + type DecisionPolicy, + FeatureObjective, + MinMaxNormalizer, + SelectTopNStrategy, + WeightedSumScoringStrategy, +} from '@rineex/decision-engine'; + +type Supplier = { + id: string; + cost: number; + quality: number; + approved: boolean; +}; +type Policy = DecisionPolicy & { + values: { + qualityWeight: number; + costWeight: number; + }; +}; + +const definition = { + id: 'supplier-selection', + version: 'v1', + candidateRefResolver: { resolve: (supplier: Supplier) => supplier.id }, + constraints: [ + { + id: 'approved', + evaluate: (supplier: Supplier) => + supplier.approved + ? { constraintId: 'approved', satisfied: true as const } + : { + constraintId: 'approved', + satisfied: false as const, + reasonCode: 'NOT_APPROVED', + }, + }, + ], + features: [ + { + key: 'quality', + objective: FeatureObjective.MAXIMIZE, + evaluate: (supplier: Supplier) => ({ + key: 'quality', + objective: FeatureObjective.MAXIMIZE, + rawValue: supplier.quality, + }), + }, + { + key: 'cost', + objective: FeatureObjective.MINIMIZE, + evaluate: (supplier: Supplier) => ({ + key: 'cost', + objective: FeatureObjective.MINIMIZE, + rawValue: supplier.cost, + }), + }, + ], + normalizer: new MinMaxNormalizer(), + scoringStrategy: new WeightedSumScoringStrategy({ + resolve: (key, policy) => + key === 'quality' + ? policy.values.qualityWeight + : policy.values.costWeight, + }), + rankingStrategy: new DescendingScoreRankingStrategy(), + selectionStrategy: new SelectTopNStrategy(1), +}; + +const execution = new DecisionEngine(new DefaultDecisionValidator()).execute( + definition, + { + candidates: [ + { id: 'low-cost', quality: 60, cost: 10, approved: true }, + { id: 'high-quality', quality: 100, cost: 90, approved: true }, + ], + context: undefined, + policy: { + id: 'default', + version: 'v1', + values: { qualityWeight: 0.75, costWeight: 0.25 }, + }, + }, +); + +console.log(execution.result.selected); +``` + +Use `InMemoryDecisionEventPublisher` only when an in-memory ordered dispatcher +fits the application. The engine itself merely returns events; publish them +after a successful execution boundary. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..2eac1b9 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,54 @@ +# Development workflow + +This workspace uses pnpm workspaces and Turborepo. Install dependencies with the +pnpm version declared by the root `package.json`. + +## Commands + +```bash +pnpm install +pnpm build +pnpm test +pnpm test:types +pnpm check-types +pnpm lint +pnpm lint:fix +pnpm prettier:write +``` + +Turborepo runs package tasks with their declared dependency order. `test` +depends on builds; `test:types` runs only packages that define type tests. + +Run a single package task with its package name: + +```bash +pnpm --filter @rineex/decision-engine test +pnpm --filter @rineex/ddd check-types +pnpm --filter @rineex/pg-slonik build +``` + +## Build and test conventions + +Most TypeScript packages use tsup and Vitest. The cookie-parser, CORS, +favicon-ignore, and response-time Nest modules use Jest; the Helmet module uses +Vitest. Source tests live alongside source files or in a package `tests` +directory. + +## Release workflow + +Changesets is configured at the workspace root. Create a release entry with +`pnpm changeset`, apply selected versions with `pnpm changeset:version`, then +publish with `pnpm changeset:publish`. Build before publishing. + +## Create a package + +The generator creates a new `packages/` library skeleton, including +TypeScript, tsup, Vitest, ESLint, and package metadata. + +```bash +pnpm g:pkg my-package "Short package description" +``` + +Names must use lowercase letters, numbers, and hyphens. The generator does not +add exports or business code; add public exports to the generated `src/index.ts` +before publishing. diff --git a/docs/nestjs-integrations.md b/docs/nestjs-integrations.md new file mode 100644 index 0000000..1ecbf7f --- /dev/null +++ b/docs/nestjs-integrations.md @@ -0,0 +1,53 @@ +# NestJS integrations + +These packages use Nest configurable modules and apply their middleware to all +routes. Install the package together with the peer dependencies shown in its +`package.json`. + +## Redis + +`RedisModule.register(options, { connection })` configures a single Redis +URL/options object or a cluster-node list. Inject the created client with +`@InjectRedis()` or `@InjectRedis('name')`. `RedisHealthModule` exposes +`RedisHealthIndicator.isHealthy(key)` for Terminus health checks. + +## PostgreSQL with Slonik + +Register each named pool and inject it by name: + +```ts +@Module({ + imports: [ + SlonikModule.register({ + connections: [{ name: 'DEFAULT', dsn: process.env.DATABASE_URL! }], + }), + ], +}) +export class AppModule {} + +class Repository { + constructor(@InjectPool() private readonly pool: DatabasePool) {} +} +``` + +Pools are verified during creation, retried on connection failure (nine retries +with a three-second delay by default), and closed during application shutdown. +`SlonikModule` is global by default. + +## HTTP middleware modules + +All modules use `register` or `registerAsync` from Nest's +`ConfigurableModuleBuilder` and attach middleware to `'*'` routes. + +| Module | Export | Underlying package | +| -------------- | --------------------- | ------------------ | +| Cookie parser | `CookieParserModule` | `cookie-parser` | +| CORS | `CorsModule` | `cors` | +| Helmet | `HelmetModule` | `helmet` | +| Ignore favicon | `IgnoreFaviconModule` | built in | +| Response time | `ResponseTimeModule` | `response-time` | + +`CorsModule` defaults to credentials enabled, standard allowed methods/headers, +and `204` for preflight responses; supplied options override those defaults. +`IgnoreFaviconModule` responds with `204` when a request URL includes +`favicon.ico`. Response-time options are `digits`, `header`, and `suffix`. diff --git a/docs/packages.md b/docs/packages.md new file mode 100644 index 0000000..af0d9ee --- /dev/null +++ b/docs/packages.md @@ -0,0 +1,23 @@ +# Package catalog + +| Package | Purpose | Package guide | +| -------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- | +| `@rineex/ddd` | Domain primitives, errors, results, and value objects | [README](../packages/ddd/README.md) | +| `@rineex/decision-engine` | Deterministic candidate evaluation and selection | [README](../packages/decision-engine/README.md) | +| `@rineex/auth-core` | Authentication domain contracts and types | [README](../packages/authentication/core/README.md) | +| `@rineex/authentication-method-otp` | OTP auth-method implementation | [README](../packages/authentication/methods/otp/README.md) | +| `@rineex/authentication-method-passwordless` | Internal passwordless challenge implementation | [README](../packages/authentication/methods/passwordless/README.md) | +| `@rineex/ioredis` | NestJS Redis connection and health support | [README](../packages/ioredis/README.md) | +| `@rineex/pg-slonik` | NestJS Slonik pool registration and injection | [README](../packages/pg-slonik/README.md) | +| `@rineex/cookie-parser-mw-module` | Cookie-parser middleware module | [README](../packages/nest/cookie-middleware-module/README.md) | +| `@rineex/cors-mw-module` | CORS middleware module | [README](../packages/nest/cors-middleware-module/README.md) | +| `@rineex/helmet-mw-module` | Helmet middleware module | [README](../packages/nest/helmet-middleware-module/README.md) | +| `@rineex/favicon-ignore-mw-module` | Favicon suppression middleware module | [README](../packages/nest/no-favicon-middleware-module/README.md) | +| `@rineex/response-time-mw-module` | Response-time middleware module | [README](../packages/nest/response-time-middleware-module/README.md) | +| `@rineex/libs` | Placeholder package with no public exports | [README](../packages/libs/README.md) | +| `@rineex/eslint-config` | Private shared ESLint flat configurations | [README](../packages/eslint-config/README.md) | +| `@rineex/typescript-config` | Private shared TypeScript configurations | [README](../packages/typescript-config/README.md) | + +The two configuration packages are private workspace dependencies. They are +consumed by repository packages, not distributed as runtime application +dependencies. diff --git a/docs/superpowers/specs/2026-08-08-ddd-primitives-design.md b/docs/superpowers/specs/2026-08-08-ddd-primitives-design.md deleted file mode 100644 index 21cdb2e..0000000 --- a/docs/superpowers/specs/2026-08-08-ddd-primitives-design.md +++ /dev/null @@ -1,55 +0,0 @@ -# DDD Primitive Hardening Design - -## Goal - -Strengthen `packages/ddd` around entity invariants, aggregate boundaries, -domain-event correctness, and explicit mapper-based rehydration. This is an -intentional breaking change. - -## Entity contract - -`Entity` stores identity and state but does not invoke overridable validation -from its base constructor. Concrete types validate after `super(...)` through -their creation/rehydration factory. Validation accepts candidate props so -`mutate` can validate before committing the replacement. A failed mutation -leaves the previous valid state intact. - -Entity metadata and state must not expose mutable internal `Date` instances. -Serialization remains explicit: `toObject()` is the domain-specific object -projection; `toJSON()` is the JSON-safe primitive projection. - -## Aggregate-root contract - -An aggregate root is the only public entry point for changes to its consistency -boundary. Child entities are not exposed for independent persistence. Event -recording is protected and verifies that the event aggregate ID equals the root -ID. Events are returned in insertion order and pulled atomically after -successful application work. - -## Domain-event contract - -Events are immutable facts. Event ID, stable event name, positive schema -version, aggregate ID, occurrence timestamp, and JSON-safe payload are required -at construction. The base class does not depend on Node-specific random-ID -generation. Event payload serialization returns a detached primitive structure. - -## Rehydration - -Mappers translate persistence data into value objects and invoke an explicit -rehydration factory. Rehydration restores state without emitting creation -events. Creation factories may emit creation events only after the new aggregate -is valid. - -## Documentation - -Update the package README with a quick start and links to -`packages/ddd/docs/DDD-GUIDE.md` and `packages/ddd/docs/MIGRATION.md`. The guide -explains where each primitive belongs, when to use it, and when not to use it. -Migration notes list the removed/changed APIs and before/after examples. - -## Testing - -Add tests for constructor validation safety, atomic failed mutations, immutable -metadata and payloads, aggregate/event identity matching, event metadata -validation, event ordering, and mapper rehydration without creation events. Run -package type tests, unit tests, lint, and build after migration. diff --git a/packages/authentication/core/Architecture.md b/packages/authentication/core/Architecture.md deleted file mode 100644 index 6f93e3b..0000000 --- a/packages/authentication/core/Architecture.md +++ /dev/null @@ -1,164 +0,0 @@ -# Auth Core — Architecture (As Implemented) - -Framework-agnostic authentication core built on `@rineex/ddd`. This document -describes **what exists in code today**, not aspirational designs. For planned -features (Flow DSL, Principal/Credential model), see [FUTURE.md](./FUTURE.md). - -## Purpose - -- Orchestrate authentication attempts via pluggable auth methods -- Enforce policy decisions before and during authentication -- Emit domain events for attempt lifecycle -- Stay independent of HTTP, databases, and token formats - -## Packages - -| Package | Role | -| -------------------------------------------- | --------------------------------------------------------------- | -| `@rineex/auth-core` | Shared identity model, attempt aggregate, policy engine, ports | -| `@rineex/authentication-method-otp` | `AuthMethodPort` adapter for OTP | -| `@rineex/authentication-method-passwordless` | Standalone challenge issue/verify (not yet on `AuthMethodPort`) | - -## Bounded Context - -**In scope:** verifying proof of access, attempt orchestration, policy -evaluation. - -**Out of scope:** user profiles, authorization/permissions, HTTP, UI, concrete -token formats, database schemas. - -## Ubiquitous Language (Implemented) - -| Term | Meaning in code | -| ------------ | -------------------------------------------------------------------- | -| Identity | Actor that can authenticate (`Identity` entity, exported) | -| Auth Attempt | One authentication execution (`AuthenticationAttempt` aggregate) | -| Auth Method | Concrete mechanism (`AuthMethod` VO; `AuthMethodPort` for plugins) | -| Auth Factor | Category of proof (`AuthFactor` VO) | -| Auth Policy | Named policy slot (`AuthPolicy` VO; evaluated by `AuthPolicyEngine`) | -| Session | Post-auth continuity (`Session` entity — in-tree, not exported) | -| MFA Session | Step-up verification context (`MFASession` aggregate — in-tree) | - -**Not implemented:** Principal, Credential, Auth Proof, TrustLevel, Flow DSL. - -> **Naming note:** Two types named `Identity` exist — an **entity** (exported) -> and an **aggregate** (in-tree, unexported). Prefer the entity in new code -> until the aggregate is published or renamed. - -## Public Surface vs In-Tree - -### Exported from `@rineex/auth-core` - -- **Aggregate:** `AuthenticationAttempt` -- **Entity:** `Identity` -- **Value objects:** `AuthAttemptId`, `AuthFactor`, `AuthMethod`, `AuthPolicy`, - `AuthStatus`, `IdentityId`, `IdentityProvider`, `RiskSignal` -- **Events:** `AuthenticationStartedEvent`, `AuthenticationFailedEvent`, - `AuthenticationSucceededEvent` -- **Policy:** `AuthPolicyEvaluator`, `AuthPolicyContext`, `AuthPolicyDecision`, - `AuthPolicyEngine` -- **Ports:** `AuthMethodPort`, `AuthenticationAttemptRepositoryPort`, - `IdentityRepository`, `DomainEventPublisherPort` - -### In-tree (not exported) - -- Domains: MFA, OAuth, session, token -- Application services: flow start/verify, MFA start/issue/verify, OAuth - authorize (stub) -- Error registries and most domain errors -- Additional repository and observability ports - -## Core Aggregates and Entities - -### AuthenticationAttempt (exported aggregate) - -Lifecycle: `pending` → `succeed` | `failed`. - -Methods: `start`, `fail`, `registerAttempt`, `succeed`. Emits started, failed, -succeeded events. - -### Identity (exported entity) - -Represents an authenticatable actor. Distinct from the unexported Identity -aggregate in `identity.aggregate.ts`. - -### Session (entity, unexported) - -Holds `identityId`, token reference, expiry, revocation. Not an aggregate root. - -### MFASession, OauthAuthorization, Token (unexported) - -Domain models exist with application services partially implemented. Not on the -public package entry. - -## Auth Method Integration - -### AuthMethodPort (OTP) - -```typescript -type AuthMethodPort = { - readonly method: AuthMethodName; - start(params: { - authAttemptId: AuthAttemptId; - ctx: unknown; - }): AuthMethodOutcome | Promise; - verify(params: { - authAttemptId: AuthAttemptId; - payload: unknown; - }): AuthMethodOutcome | Promise; -}; -``` - -`OtpAuthMethod` in `@rineex/authentication-method-otp` implements this port. - -### Passwordless (standalone) - -`IssuePasswordlessChallengeService` and `VerifyPasswordlessChallengeService` -live in the passwordless package. They are **not** wired through -`AuthMethodPort` today. See passwordless package README. - -## Policy Engine - -`AuthPolicyEngine` runs registered `AuthPolicyEvaluator` instances. First deny -wins. Evaluators may return `requiresStepUp` for MFA escalation. No declarative -policy DSL — evaluators are code. - -## Error Pattern - -Per bounded context: - -1. Const registry: `AuthCoreErrorRegistry` with namespaces (`AUTH_CORE_MFA`, - etc.) -2. Error classes extend `DomainError<'NAMESPACE.CODE', Meta>` -3. Architecture tests verify every error code is registered - -See [src/domain/errors/README.md](./src/domain/errors/README.md). - -## Layering - -``` -┌─────────────────────────────────────┐ -│ Application (flow + MFA services) │ unexported -├─────────────────────────────────────┤ -│ Domain (attempt, identity, policy) │ partially exported -├─────────────────────────────────────┤ -│ Ports (inbound + outbound) │ partially exported -└─────────────────────────────────────┘ - ▲ - │ adapters (consumer-provided) -``` - -## Hard Rules (still apply) - -1. Authentication is not identity management -2. Auth methods are plugins (`AuthMethodPort`) -3. Infrastructure stays outside domain -4. Domain owns invariants only -5. New methods must not require changing `AuthenticationAttempt` - -## Related Docs - -- [Definition.md](./Definition.md) — contracts and registries -- [RULES.md](./RULES.md) — package topology and port map -- [GAP_ANALYSIS.md](./GAP_ANALYSIS.md) — remaining work -- [ONBOARDING.md](./ONBOARDING.md) — contributor guide diff --git a/packages/authentication/core/Definition.md b/packages/authentication/core/Definition.md deleted file mode 100644 index fece4e0..0000000 --- a/packages/authentication/core/Definition.md +++ /dev/null @@ -1,125 +0,0 @@ -# Auth Core — Definition (As Implemented) - -Contracts, registries, and integration patterns that exist in code today. For -aspirational Flow DSL, proof system, and Principal/Credential model, see -[FUTURE.md](./FUTURE.md). - -## AuthMethodPort - -All auth method modules should implement this inbound port: - -| Method | Purpose | -| -------- | ------------------------------------------------------------------ | -| `start` | Initiate the method (send OTP, redirect to OAuth, issue challenge) | -| `verify` | Verify a response (OTP code, callback payload) | - -Returns `AuthMethodOutcome` — `{ ok: true }` or -`{ ok: false, violation: UseCaseError }`. This is **not** `Result`; `Result` -is used at the application layer. - -**Implemented adapter:** `OtpAuthMethod` (`@rineex/authentication-method-otp`). - -**Not yet adapted:** passwordless (standalone services only). - -## Flow Orchestration - -`StartAuthenticationFlowApplicationService` and -`VerifyAuthenticationFlowApplicationService` resolve a registered -`AuthMethodPort` and delegate to `start` / `verify` on an -`AuthenticationAttempt`. This is **not** a data-driven Flow DSL — it is direct -method dispatch. - -## Policy Evaluation - -| Type | Role | -| --------------------- | ------------------------------------------------ | -| `AuthPolicyEvaluator` | Evaluates one policy; returns allow/deny/step-up | -| `AuthPolicyEngine` | Runs evaluators; first deny wins | -| `AuthPolicyContext` | Input snapshot for evaluation | -| `AuthPolicyDecision` | Outcome: allowed, denied, or requires step-up | - -Policies are registered by name via `AuthPolicyRegistry` (currently `base` -only). - -## Type Registries - -Closed registries extended via module augmentation: - -| Registry | Current values | -| -------------------------- | ---------------------------------------------------- | -| `AuthMethodRegistry` | `passwordless`, `otp` (augmented by method packages) | -| `AuthFactorRegistry` | `password` | -| `AuthPolicyRegistry` | `base` | -| `IdentityProviderRegistry` | (empty) | -| `RiskSignalRegistry` | (empty) | - -## Error Registry - -`AuthCoreErrorRegistry` namespaces: - -- `AUTH_CORE_MFA` — challenge/session MFA errors -- `AUTH_CORE_OAUTH` — OAuth authorization errors -- `AUTH_CORE_IDENTITY` — identity state errors -- `AUTH_CORE_ATTEMPT` — attempt failures and not-found -- `AUTH_CORE_SESSION` — session invalid -- `AUTH_CORE_TOKEN` — token invalid -- `AUTH_CORE_SCOPE` — scope invalid - -Pattern: - -```typescript -import { DomainError, InferErrorCodes, Metadata } from '@rineex/ddd'; - -export const AuthCoreErrorRegistry = { AUTH_CORE_TOKEN: ['INVALID'] } as const; -export type AuthCoreDomainErrorCode = InferErrorCodes< - typeof AuthCoreErrorRegistry ->; - -class InvalidAuthTokenError extends DomainError< - 'AUTH_CORE_TOKEN.INVALID', - Metadata<{ actualLength: number }> -> { - readonly code = 'AUTH_CORE_TOKEN.INVALID' as const; -} -``` - -## Authentication Attempt States - -| Status | Meaning | -| --------- | ------------------------ | -| `pending` | Attempt in progress | -| `succeed` | Authentication succeeded | -| `failed` | Authentication failed | - -## Outbound Ports (exported) - -| Port | Responsibility | -| ------------------------------------- | --------------------- | -| `AuthenticationAttemptRepositoryPort` | Persist attempts | -| `IdentityRepository` | Load identities | -| `DomainEventPublisherPort` | Publish domain events | - -## Outbound Ports (in-tree) - -`SessionRepositoryPort`, `TokenRepository`, `MfaSessionRepository`, -`MfaSessionIdGenerator`, `MfaClock`, `LoggerPort`, `ObservabilityEventPort`. - -## MFA Application Services (in-tree) - -- `StartMfaSessionApplicationService` -- `IssueMfaChallengeApplicationService` -- `VerifyMfaApplicationService` - -## OAuth (in-tree, incomplete) - -Domain model for OAuth authorization with PKCE value objects exists. -`OAuthAuthorizeService` is a stub. - -## Passwordless Package - -Separate package with its own aggregate, error registry, and issue/verify -services. See -[methods/passwordless/README.md](../methods/passwordless/README.md). - -**Known gap:** package `index.ts` does not export runtime API — only type -augmentation. See [GAP_ANALYSIS.md](./GAP_ANALYSIS.md). diff --git a/packages/authentication/core/Develop.md b/packages/authentication/core/Develop.md deleted file mode 100644 index e69de29..0000000 diff --git a/packages/authentication/core/FUTURE.md b/packages/authentication/core/FUTURE.md deleted file mode 100644 index 0146999..0000000 --- a/packages/authentication/core/FUTURE.md +++ /dev/null @@ -1,1495 +0,0 @@ -# Future Work — Aspirational Auth Design - -> Preserved from the original Definition.md. This describes target-state -> features not yet implemented. See Definition.md for what exists today. - -## Step 1 — Define **Auth Method SPI (Plugin Contract)** - -This is the **most critical step**. If this is wrong, everything after becomes -rigid. - ---- - -# Auth Method SPI — Domain-Level Contract - -## 1. Goal of This Step - -Define **how authentication methods plug into the core** without: - -- Modifying domain logic -- Adding conditionals (`if password`, `if oauth`) -- Leaking protocol / provider / infra concerns -- Forcing storage or transport choices - -This SPI must support: - -- Password -- Passwordless -- OTP -- OAuth / OIDC -- Social login -- Future methods (passkeys, DID, etc.) - ---- - -## 2. What an Auth Method IS (and is NOT) - -### ✅ IS - -- A **capability descriptor** -- A **set of required inputs** -- A **type of proof produced** -- A **participant in a flow** - -### ❌ IS NOT - -- A service with business logic -- A protocol implementation -- A provider SDK wrapper -- A controller or handler -- A persistence concern - -If logic creeps in here → stop. - ---- - -## 3. Core Domain Abstractions - -### 3.1 AuthMethodType (Value Object) - -Represents **identity**, not behavior. - -```ts -AuthMethodType -- value: string -``` - -Rules: - -- Stable identifier -- Configurable -- No enums hardcoded in domain - -Examples: - -- `password` -- `passwordless_email` -- `otp_totp` -- `oauth_oidc` -- `social_google` - ---- - -### 3.2 AuthMethodDefinition (Domain Object) - -This is the **plugin contract**. - -```ts -AuthMethodDefinition -- type: AuthMethodType -- supportedFactors: Set -- requiredInputs: Set -- outputProof: AuthProofType -- challengeCapable: boolean -``` - -This object: - -- Is immutable -- Is registered at startup -- Contains **no logic** - ---- - -## 4. Auth Inputs (What the method needs) - -Auth methods declare **what they need**, not _how it arrives_. - -```ts -AuthInputType - - identifier - - secret - - otp - - assertion - - authorization_code - - device_context - - redirect_context; -``` - -Rules: - -- Transport-agnostic -- Serializable -- Validated at boundaries - ---- - -## 5. Auth Proof (What the method produces) - -Auth methods **do not authenticate**. They produce **proof**. - -```ts -AuthProofType - password_proof - otp_proof - oauth_proof - assertion_proof; -``` - -The **domain decides** what to do with the proof. - ---- - -## 6. Verification Responsibility (Important) - -Auth Methods **do not verify themselves**. - -Verification is done via **ports**: - -```ts -AuthProofVerifierPort -- verify(proof, context): VerificationResult -``` - -This allows: - -- Swapping providers -- Mocking -- Enterprise overrides -- Multiple verification strategies - ---- - -## 7. Method Registration Model - -Auth methods are **registered**, not hardcoded. - -```ts -AuthMethodRegistry - register(definition) - get(type) - list(); -``` - -Rules: - -- Registry is read-only at runtime -- Domain depends only on interface -- Infrastructure provides implementations - ---- - -## 8. Example (Conceptual, Not Implementation) - -| Method | Inputs | Proof | Factor | -| ---------- | ------------------- | --------------- | ---------- | -| Password | identifier + secret | password_proof | knowledge | -| Magic Link | identifier | assertion_proof | possession | -| OTP | identifier + otp | otp_proof | possession | -| OAuth | authorization_code | oauth_proof | delegated | -| API Token | token | assertion_proof | possession | - -Notice: - -- No HTTP -- No redirects -- No JWT -- No providers - ---- - -## 9. What This Enables - -With this SPI, you can: - -- Add a new auth method **without touching domain logic** -- Compose methods into MFA -- Enable / disable methods via config -- Support enterprise overrides -- Keep core stable for years - ---- - -## 10. Hard Validation Checklist - -Before moving forward, confirm: - -- No logic inside method definitions -- No branching by method type -- No enum explosion -- No provider coupling -- No storage assumptions - -If any fails → redesign now. - ---- - -## Step 1 Status - -✅ Auth Method SPI defined ✅ Extensible ✅ Domain-pure ✅ Hexagonal-safe - ---- - -# Step 2 — Authentication Flow DSL (Orchestration Model) - -This step defines **how authentication happens**, without: - -- Hard-coding flows -- Coupling to methods -- Assuming HTTP redirects -- Embedding logic in code paths - -If flows are not modeled explicitly, you will **never** support enterprise auth -cleanly. - ---- - -## 1. Purpose of Authentication Flow - -An **Authentication Flow** answers one question: - -> _“In what order, under what conditions, and with which rules are auth methods -> executed?”_ - -It must support: - -- Single-step auth (password, social) -- Multi-step auth (OTP, MFA) -- Conditional auth (risk-based) -- Enterprise SSO -- Future orchestration (passkeys, step-up) - ---- - -## 2. Core Principle (Non-Negotiable) - -**Flows are data, not code.** - -No: - -```ts -if (method === 'password') { ... } -``` - -Instead: - -- Declarative steps -- Explicit transitions -- Policy-driven decisions - ---- - -## 3. Flow as a Domain Concept - -### AuthenticationFlow (Domain Object) - -```ts -AuthenticationFlow -- flowId -- name -- steps: AuthFlowStep[] -- entryConditions -- exitConditions -``` - -Rules: - -- Immutable -- Versionable -- Serializable -- Loaded at startup or via config - ---- - -## 4. Flow Step Model - -### AuthFlowStep - -```ts -AuthFlowStep -- stepId -- authMethodType -- required: boolean -- onSuccess: Transition -- onFailure: Transition -- onChallenge?: Transition -``` - -Each step: - -- Executes **exactly one Auth Method** -- Produces a proof -- Does NOT decide success globally - ---- - -## 5. Transition Model - -### Transition - -```ts -Transition -- targetStepId | terminalState -- condition?: PolicyExpression -``` - -Terminal states: - -- `AUTHENTICATED` -- `FAILED` -- `CHALLENGED` - -This enables: - -- Retry -- Step-up -- Short-circuiting - ---- - -## 6. Policy Expressions (Referenced, Not Implemented Yet) - -Flows do **not** evaluate logic themselves. - -They reference policies: - -```ts -PolicyExpression - policyId - parameters; -``` - -Examples: - -- `risk.score > threshold` -- `principal.trustLevel < required` -- `device.notTrusted` - -(Policy language comes in Step 4.) - ---- - -## 7. AuthenticationAttempt Lifecycle (Flow-Driven) - -The flow **drives the attempt**, not the other way around. - -States: - -- `Initialized` -- `InProgress` -- `AwaitingChallenge` -- `Succeeded` -- `Failed` - -Rules: - -- One flow per attempt -- Steps are executed sequentially -- Proofs are accumulated -- Finalization happens only at terminal state - ---- - -## 8. Example Flows (Conceptual) - -### 8.1 Simple Password Flow - -``` -Step 1: password - onSuccess → AUTHENTICATED - onFailure → FAILED -``` - ---- - -### 8.2 Password + OTP (MFA) - -``` -Step 1: password - onSuccess → Step 2 - onFailure → FAILED - -Step 2: otp - onSuccess → AUTHENTICATED - onFailure → FAILED -``` - ---- - -### 8.3 Risk-Based Step-Up - -``` -Step 1: password - onSuccess → - if risk.low → AUTHENTICATED - if risk.high → Step 2 - -Step 2: otp - onSuccess → AUTHENTICATED - onFailure → FAILED -``` - ---- - -### 8.4 OAuth / Social Login - -``` -Step 1: oauth_oidc - onSuccess → AUTHENTICATED - onFailure → FAILED - onChallenge → AWAITING_EXTERNAL_ASSERTION -``` - -No redirects modeled. That’s infrastructure. - ---- - -## 9. What Flows Explicitly Do NOT Know - -Flows do **not** know about: - -- HTTP redirects -- UI screens -- Cookies -- Tokens -- OAuth providers -- Mobile vs web -- DB schemas - -They only know: - -- Steps -- Transitions -- Policies - ---- - -## 10. Extensibility Guarantees - -With this DSL, you can: - -- Add new auth methods without new flows -- Add new flows without code changes -- Customize enterprise flows via config -- Reuse flows across products -- Audit authentication behavior deterministically - ---- - -## 11. Validation Checklist (Must Pass) - -Before moving forward: - -✔ Flow is declarative ✔ No branching by method type ✔ No protocol assumptions ✔ -No infrastructure knowledge ✔ Supports MFA and step-up ✔ Serializable and -versionable - -If any fails → redesign. - ---- - -## Step 2 Status - -✅ Authentication Flow DSL defined ✅ Orchestration decoupled from methods ✅ -Enterprise-ready ✅ Hexagonal-safe - ---- - -# Step 3 — **AuthenticationAttempt Lifecycle (Aggregate Rules)** - -This step defines **the heart of correctness**. If this aggregate is weak, -you’ll get replay attacks, broken MFA, and inconsistent auth state. - ---- - -## 1. Purpose of AuthenticationAttempt - -An **AuthenticationAttempt** represents: - -> _One and only one execution of an authentication flow._ - -It exists to: - -- Enforce invariants -- Track progress across steps -- Collect proofs -- Coordinate challenges -- Produce a single outcome - -It is the **source of truth** for authentication state. - ---- - -## 2. Why This Must Be an Aggregate Root - -Because it controls **critical invariants**: - -- A step cannot be skipped -- A proof cannot be reused -- An attempt cannot succeed twice -- A failed attempt cannot be resumed -- MFA ordering must be enforced - -If this is not an aggregate → bugs are guaranteed. - ---- - -## 3. AuthenticationAttempt Structure (Conceptual) - -**Aggregate Root: AuthenticationAttempt** - -**Key Attributes** - -- AttemptId -- FlowId -- PrincipalId? (optional initially) -- CurrentStepId -- Status -- CollectedProofs -- StartedAt -- CompletedAt? - ---- - -## 4. Attempt States (Strict State Machine) - -Only these states are allowed: - -1. `Initialized` -2. `InProgress` -3. `AwaitingChallenge` -4. `Succeeded` -5. `Failed` - -No others. No “partial success”. - ---- - -## 5. State Transition Rules (Non-Negotiable) - -### 5.1 Initialization - -- Created with a Flow -- No proofs -- No session -- Status = `Initialized` - -Allowed transitions: - -- → `InProgress` - ---- - -### 5.2 InProgress - -- Actively executing a flow step -- Accepts exactly **one proof per step** - -Allowed transitions: - -- → `AwaitingChallenge` -- → `Succeeded` -- → `Failed` - ---- - -### 5.3 AwaitingChallenge - -Used when: - -- External action is required -- OTP sent -- OAuth assertion pending -- Push approval pending - -Rules: - -- No new step allowed -- Only challenge response accepted - -Allowed transitions: - -- → `InProgress` -- → `Failed` - ---- - -### 5.4 Succeeded (Terminal) - -Rules: - -- Immutable -- Produces AuthenticationSession -- No further actions allowed - ---- - -### 5.5 Failed (Terminal) - -Rules: - -- Immutable -- No retry inside same attempt -- New attempt required - ---- - -## 6. Proof Handling Rules - -Proofs are **append-only**. - -Rules: - -- One proof per step -- Proof must match expected AuthMethodType -- Proof must be verified before progressing -- Proofs cannot be modified or deleted - -This guarantees: - -- Auditability -- Determinism -- Replay prevention - ---- - -## 7. Step Advancement Rules - -The aggregate enforces: - -- Steps must be executed in order -- Transition conditions must be satisfied -- Skipping steps is forbidden -- Optional steps are flow-defined, not attempt-defined - -The attempt **does not decide** what the next step is. It **asks the flow**. - ---- - -## 8. Failure Semantics (Important) - -Failures are **final**. - -Examples: - -- Wrong password → Failed -- OTP expired → Failed -- OAuth assertion invalid → Failed - -Retries: - -- Require a **new AuthenticationAttempt** -- Rate limiting is policy/infrastructure concern - -This avoids: - -- State confusion -- Timing attacks -- Complex rollback logic - ---- - -## 9. Relationship to Other Aggregates - -- Uses **AuthenticationFlow** (read-only) -- Produces **AuthenticationSession** -- References **Principal**, but does not own it -- Uses **Policies**, but does not evaluate them - -No bidirectional coupling. - ---- - -## 10. What AuthenticationAttempt Does NOT Do - -Explicitly forbidden: - -- Creating credentials -- Issuing tokens -- Storing secrets -- Evaluating risk -- Talking to HTTP -- Calling providers - -It enforces **process**, not mechanics. - ---- - -## 11. Invariant Checklist (Must Always Hold) - -✔ One flow per attempt ✔ One terminal state only ✔ No step skipping ✔ No proof -reuse ✔ No resurrection after failure ✔ Deterministic outcome - -If any invariant breaks → security issue. - ---- - -## 12. Step 3 Status - -✅ Aggregate boundaries defined ✅ State machine enforced ✅ MFA-safe ✅ -Replay-safe ✅ Audit-ready - ---- - -# Step 4 — **Authentication Policy Model (Risk, Conditions, Step-Up)** - -This step defines **who is allowed to authenticate, under what conditions, and -with which strength** — without hard-coding logic. - -If policies are not modeled cleanly, you will: - -- Hard-code MFA rules -- Fork enterprise behavior -- Break extensibility -- Lose auditability - ---- - -## 1. Purpose of Policy in Auth Domain - -A **Policy** answers questions like: - -- Is this authentication allowed? -- Is step-up required? -- Which flow should apply? -- Is this context risky? -- Is the current proof sufficient? - -Policies **do not authenticate**. They **constrain and steer** authentication. - ---- - -## 2. Core Principle (Non-Negotiable) - -**Policies are declarative and evaluative — never procedural.** - -No: - -```ts -if (ip === 'x') requireOtp(); -``` - -Yes: - -- Policy definitions -- Policy evaluation results -- Policy-driven decisions - ---- - -## 3. Policy as a First-Class Domain Concept - -### AuthenticationPolicy (Domain Object) - -```ts -AuthenticationPolicy -- policyId -- name -- scope -- rules: PolicyRule[] -- effect -``` - -Rules: - -- Immutable -- Versionable -- Serializable -- Configurable per tenant / environment - ---- - -## 4. Policy Scope - -Policies apply at different levels: - -```ts -PolicyScope = - | Global - | Principal - | AuthMethod - | AuthFlow - | Resource -``` - -Examples: - -- Global MFA enforcement -- Principal-specific restrictions -- Method-specific constraints -- Flow selection rules - ---- - -## 5. Policy Rules (Atomic Conditions) - -### PolicyRule - -```ts -PolicyRule -- condition: ConditionExpression -- action: PolicyAction -``` - -Each rule: - -- Evaluates **one condition** -- Produces **one action** -- Has no side effects - ---- - -## 6. Condition Model (What can be checked) - -Conditions evaluate **context**, never infrastructure. - -```ts -ConditionExpression - subject - operator - value; -``` - -### Common Subjects - -- risk.score -- principal.trustLevel -- device.trusted -- location.country -- time.window -- auth.method -- auth.factor -- attempt.count - -### Operators - -- equals -- notEquals -- greaterThan -- lessThan -- in -- notIn - ---- - -## 7. Policy Actions (What can be enforced) - -Actions **do not perform authentication**. - -```ts -PolicyAction = - | Allow - | Deny - | RequireStepUp - | SelectFlow - | LimitTrustLevel -``` - -Examples: - -- Require OTP if risk > threshold -- Deny auth from blocked country -- Force enterprise SSO -- Downgrade session trust - ---- - -## 8. Policy Evaluation Result - -Policies return **decisions**, not behavior. - -```ts -PolicyEvaluationResult -- decision -- requiredFlow? -- requiredAuthFactors? -- maxTrustLevel? -- reasons[] -``` - -This allows: - -- Explainability -- Auditing -- Compliance - ---- - -## 9. Policy Evaluation Responsibility - -Policies are **evaluated by a Domain Service**, not entities. - -```ts -AuthenticationPolicyEvaluator -- evaluate(context): PolicyEvaluationResult -``` - -Context includes: - -- Principal snapshot -- AuthenticationAttempt snapshot -- Device/context snapshot -- Risk assessment (via port) - ---- - -## 10. Relationship with Authentication Flow - -Policies can: - -- Select a flow -- Alter transitions -- Require additional steps - -Flows **reference policies**, but do not contain logic. - -This separation allows: - -- Same flow, different behavior -- Enterprise overrides -- Runtime reconfiguration - ---- - -## 11. Example Policies (Conceptual) - -### 11.1 Risk-Based MFA - -``` -IF risk.score > 70 -THEN RequireStepUp (otp) -``` - ---- - -### 11.2 Geo Restriction - -``` -IF location.country IN blockedCountries -THEN Deny -``` - ---- - -### 11.3 Trust Downgrade - -``` -IF device.trusted = false -THEN LimitTrustLevel = LOW -``` - ---- - -### 11.4 Flow Selection - -``` -IF principal.type = Service -THEN SelectFlow = m2m_flow -``` - ---- - -## 12. What Policies Explicitly Do NOT Do - -Forbidden: - -- Issuing challenges -- Sending OTPs -- Creating sessions -- Calling providers -- Accessing databases directly -- Knowing HTTP headers - -They only **decide**. - ---- - -## 13. Extensibility Guarantees - -With this model you can: - -- Add new conditions without changing flows -- Add new actions without breaking domain -- Support enterprise policy engines later -- Audit decisions deterministically - ---- - -## 14. Step 4 Status - -✅ Policy model defined ✅ Risk-adaptive ready ✅ Enterprise-grade ✅ -Explainable and auditable - ---- - -# Step 5 — **Credential Lifecycle Modeling** - -This step defines **what a credential is**, how it is created, validated, -rotated, revoked, and expired — without storing secrets or coupling to storage. - -If credential lifecycle is vague, security degrades fast. - ---- - -## 1. Purpose of Credential in Auth Domain - -A **Credential** represents: - -> _A verifiable authentication capability bound to a Principal._ - -It is **not**: - -- A password hash -- An OTP secret -- A token -- A provider artifact - -Those are **infrastructure details**. - ---- - -## 2. Credential as a Domain Entity - -### Credential (Entity) - -```ts -Credential -- CredentialId -- PrincipalId -- AuthMethodType -- AuthFactorType -- Status -- IssuedAt -- ExpiresAt? -- LastUsedAt? -- Metadata -``` - ---- - -## 3. Credential Status (Finite State) - -```ts -CredentialStatus = - | Active - | Suspended - | Revoked - | Expired - | Compromised -``` - -Rules: - -- Revoked and Compromised are terminal -- Expired is time-based -- Suspended is reversible - ---- - -## 4. Credential Invariants (Strict) - -A credential must always satisfy: - -- Belongs to exactly one Principal -- Is bound to exactly one Auth Method -- Cannot be reused across principals -- Cannot be reactivated after Revocation -- Cannot authenticate if not Active -- Cannot exist without lifecycle metadata - -Violation = security bug. - ---- - -## 5. Credential Creation Rules - -Credential creation: - -- Is explicit -- Requires policy approval -- Produces **no secret** in domain -- Delegates material generation to infrastructure - -Example: - -- Domain: “Create OTP credential” -- Infra: Generates secret, stores it securely - ---- - -## 6. Credential Usage Rules - -On each successful authentication: - -- Credential status must be Active -- Expiration must be checked -- Usage timestamp may be updated -- Compromise signals may suspend or revoke - -Credential usage is **observed**, not enforced here. - ---- - -## 7. Credential Rotation & Replacement - -Rotation is modeled as: - -- Create new credential -- Activate new -- Suspend or revoke old - -No mutation-in-place. - -This allows: - -- Auditability -- Rollback -- Compliance - ---- - -## 8. Credential Revocation - -Revocation reasons: - -- User action -- Admin action -- Policy decision -- Risk detection -- Breach response - -Revocation: - -- Is immediate -- Is irreversible -- Must propagate to infra adapters - ---- - -## 9. Credential Expiration - -Expiration: - -- Is time-based -- Evaluated at authentication time -- Does not delete credential -- Transitions to Expired - -Deletion is **not** domain responsibility. - ---- - -## 10. Relationship to AuthenticationAttempt - -- Attempts **reference** credentials -- Attempts do not own credentials -- Credential state is checked during verification -- No attempt may mutate credential directly - ---- - -## 11. Credential Types (Examples) - -| Method | Credential Meaning | -| --------- | ---------------------- | -| Password | Knowledge credential | -| OTP | Possession credential | -| OAuth | Delegated credential | -| API Token | Possession credential | -| Passkey | Possession + inherence | - -Types are **data**, not inheritance. - ---- - -## 12. What Credential Does NOT Know - -Explicitly forbidden: - -- Hash algorithms -- OTP secrets -- Token formats -- Storage engines -- Providers -- Encryption - -Those live behind ports. - ---- - -## 13. Ports Related to Credentials - -```ts -CredentialRepository; -CredentialMaterialStore(infra); -CredentialUsageReporter; -CredentialRevocationPublisher; -``` - -Domain only depends on interfaces. - ---- - -## 14. Extensibility Guarantees - -With this model: - -- New credential types require no domain change -- Infra can store secrets however it wants -- Enterprise policies can control lifecycle -- Auditing is deterministic - ---- - -## 15. Step 5 Status - -✅ Credential lifecycle defined ✅ Security invariants enforced ✅ -Storage-agnostic ✅ Enterprise-ready - ---- - -We continue. This is the **last foundational domain step**. - ---- - -# Step 6 — **AuthenticationSession & Trust Levels** - -This step defines **what “being authenticated” actually means** in your system, -independent of tokens, cookies, or transport. - -If this is weak, authorization, SSO, and enterprise security all collapse. - ---- - -## 1. Purpose of AuthenticationSession - -An **AuthenticationSession** represents: - -> _A time-bounded, trust-scoped result of a successful authentication._ - -It is: - -- The output of Authentication -- The input to Authorization -- The anchor for SSO, re-auth, and step-up - -It is **not**: - -- A cookie -- A JWT -- A refresh token -- An HTTP session - -Those are representations. - ---- - -## 2. AuthenticationSession as Aggregate Root - -### AuthenticationSession (Aggregate Root) - -```ts -AuthenticationSession - - SessionId - - PrincipalId - - FlowId - - IssuedAt - - ExpiresAt - - TrustLevel - - AuthFactorsUsed - - ContextSnapshot - - Status; -``` - ---- - -## 3. Session Status (Finite State) - -```ts -SessionStatus = - | Active - | Expired - | Revoked -``` - -Rules: - -- Expired is time-based -- Revoked is explicit and terminal -- Active is the only usable state - ---- - -## 4. Trust Level (Critical Concept) - -### TrustLevel (Value Object) - -Trust is **not binary**. - -```ts -TrustLevel = - | Anonymous - | Low - | Medium - | High -``` - -Interpretation: - -- **Anonymous** → unauthenticated -- **Low** → weak auth (magic link, social) -- **Medium** → single strong factor -- **High** → MFA / hardware-backed - -Trust is: - -- Computed from proofs + policies -- Stored in session -- Used by authorization - ---- - -## 5. Trust Level Invariants - -- TrustLevel is **assigned at session creation** -- TrustLevel can be **downgraded** -- TrustLevel can **never be upgraded in-place** -- Upgrade requires **new AuthenticationAttempt** - -This prevents silent privilege escalation. - ---- - -## 6. Relationship to AuthenticationAttempt - -- Exactly one session per successful attempt -- Attempt produces session -- Session references attempt context (read-only) -- Session lifecycle is independent afterward - -No circular dependency. - ---- - -## 7. Context Snapshot (Why It Exists) - -### ContextSnapshot (Value Object) - -Captures authentication context **at time of issuance**: - -- Device trust -- Location -- Risk score -- Auth methods used -- Policies applied - -Why: - -- Auditability -- Compliance -- Forensic analysis -- Future policy checks - -Context is **immutable**. - ---- - -## 8. Session Expiration Rules - -Expiration is: - -- Deterministic -- Policy-driven -- Evaluated by infrastructure -- Reflected in domain state - -Domain does not: - -- Refresh sessions -- Slide expiration -- Issue refresh tokens - -Those are adapters. - ---- - -## 9. Session Revocation - -Revocation triggers: - -- User logout -- Admin action -- Credential revocation -- Risk detection -- Policy violation - -Revocation: - -- Is explicit -- Is immediate -- Invalidates all representations - ---- - -## 10. SSO Compatibility (Important) - -SSO is simply: - -- Multiple representations -- One AuthenticationSession - -This model supports: - -- Web SSO -- Mobile SSO -- API SSO -- Cross-app SSO - -Without special logic. - ---- - -## 11. What AuthenticationSession Does NOT Know - -Explicitly forbidden: - -- JWT claims -- Cookies -- Headers -- OAuth tokens -- HTTP -- Storage engine - -It only knows **meaning**, not format. - ---- - -## 12. Ports Related to Sessions - -```ts -AuthenticationSessionRepository; -SessionRevocationPublisher; -SessionExpirationScheduler; -SessionRepresentationFactory(infra); -``` - -Domain defines contracts only. - ---- - -## 13. Extensibility Guarantees - -With this model: - -- You can change token formats freely -- You can support SSO without refactor -- You can add step-up auth cleanly -- You can audit auth decisions years later - ---- - -## 14. Step 6 Status - -✅ AuthenticationSession modeled ✅ Trust levels explicit ✅ Step-up safe ✅ -Authorization-ready ✅ Enterprise-grade - ---- - -# 🎯 Foundation Complete - -You now have a **complete, extensible, production-grade Auth Domain**: - -1. Auth Method SPI -2. Authentication Flow DSL -3. AuthenticationAttempt lifecycle -4. Policy model -5. Credential lifecycle -6. AuthenticationSession & Trust diff --git a/packages/authentication/core/GAP_ANALYSIS.md b/packages/authentication/core/GAP_ANALYSIS.md deleted file mode 100644 index e56a31b..0000000 --- a/packages/authentication/core/GAP_ANALYSIS.md +++ /dev/null @@ -1,47 +0,0 @@ -# Auth Core — Future Work Tracker - -Gaps between the **current implementation** and a complete authentication -platform. Aspirational designs are preserved in [FUTURE.md](./FUTURE.md). - -## Code Gaps (priority order) - -| # | Gap | Impact | -| --- | ----------------------------------------------- | --------------------------------------------------------------- | -| 1 | Passwordless not on `AuthMethodPort` | Two integration patterns; flow services cannot use passwordless | -| 2 | Passwordless `index.ts` exports nothing | README API not importable from package entry | -| 3 | MFA/OAuth/session/token domains unexported | Consumers cannot use in-tree models without deep imports | -| 4 | `OAuthAuthorizeService` is a stub | OAuth flow incomplete at application layer | -| 5 | Email/SMS channel stubs throw `not implemented` | Passwordless delivery not wired | -| 6 | Two `Identity` types (entity vs aggregate) | Naming collision confuses contributors | -| 7 | `Token` aggregate throws plain `Error` | Inconsistent with registry-backed `DomainError` pattern | -| 8 | `OtpCode` in core throws generic `Error` | Should use domain error class | - -## Documentation Gaps (addressed in this sync) - -- Design docs described Principal/Credential/Flow DSL — rewritten to match code -- Auth errors README documented removed `type` field — updated for v6 registry -- Passwordless docs used `Result.fail` / `getValue()` — updated to v5 API -- Root README had broken ddd anchors and wrong project tree — fixed - -## Integration Gaps - -| Desired flow | Blocker | -| ----------------------------------------- | ---------------------------------------------------- | -| End-to-end OTP login via flow services | Needs consumer adapters for repos + `OtpChannelPort` | -| End-to-end passwordless via flow services | Needs `PasswordlessAuthMethod` adapter | -| MFA step-up after policy deny | MFA services exist but are unexported | -| OAuth authorization code flow | Domain exists; app service incomplete | - -## Export Tiers (proposed) - -| Tier | Contents | -| ------------ | ------------------------------ | -| Stable | Current `index.ts` exports | -| Experimental | MFA, OAuth subpath exports | -| Internal | Everything else until promoted | - -## Related - -- [IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md) — actionable tasks -- [ROADMAP.md](./ROADMAP.md) — phased timeline -- [FUTURE.md](./FUTURE.md) — original aspirational specification diff --git a/packages/authentication/core/IMPLEMENTATION_PLAN.md b/packages/authentication/core/IMPLEMENTATION_PLAN.md deleted file mode 100644 index ebe7049..0000000 --- a/packages/authentication/core/IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,51 +0,0 @@ -# Auth Core — Implementation Plan - -Open work items only. Completed fiction removed. - -## 1. Passwordless integration - -1. Export `IssuePasswordlessChallengeService`, - `VerifyPasswordlessChallengeService`, aggregate, and ports from - `methods/passwordless/src/index.ts` -2. Create `PasswordlessAuthMethod` implementing `AuthMethodPort` -3. Register in flow services alongside `OtpAuthMethod` -4. Add integration tests for attempt → passwordless → succeed path - -## 2. Export hygiene - -1. Document export tiers in README (stable / experimental / internal) -2. Add subpath exports for MFA and OAuth OR promote to main entry -3. Export `AuthCoreErrorRegistry` and error classes for consumer handling -4. Rename or remove duplicate `Identity` aggregate - -## 3. OAuth completion - -1. Implement `OAuthAuthorizeService.execute` with repository port -2. Export OAuth domain types and ports -3. Add OAuth method package or extend core adapter - -## 4. Error consistency - -1. Migrate `Token` aggregate to `EntityValidationError` / registry errors -2. Migrate `OtpCode` validation to domain error class -3. Ensure all new errors pass `auth-core-error.architecture.spec.ts` - -## 5. Channel implementations - -1. Replace `EmailChannel` / `SMSChannelImp` stubs with real adapters or remove -2. Document consumer responsibility for channel delivery - -## Out of scope (see FUTURE.md) - -- Flow DSL compiler -- Principal/Credential aggregates -- Trust level model -- Package split into core-domain / core-ports - -## Testing approach - -- Architecture tests for error registry coverage (existing pattern) -- Application service tests with port mocks (see passwordless package) -- No tests for documentation-only changes - -See [TASK_BREAKDOWN.md](./TASK_BREAKDOWN.md) for issue-sized slices. diff --git a/packages/authentication/core/ONBOARDING.md b/packages/authentication/core/ONBOARDING.md deleted file mode 100644 index b9d5891..0000000 --- a/packages/authentication/core/ONBOARDING.md +++ /dev/null @@ -1,57 +0,0 @@ -# Auth Core — Contributor Onboarding - -Welcome. Start here before diving into source. - -## 1. Read the docs (in order) - -1. [README.md](./README.md) — what is exported vs in-tree -2. [Architecture.md](./Architecture.md) — domain model as implemented -3. [Definition.md](./Definition.md) — ports, registries, contracts -4. [RULES.md](./RULES.md) — package topology -5. [GAP_ANALYSIS.md](./GAP_ANALYSIS.md) — what is still missing - -For aspirational designs (Flow DSL, Principal model), see -[FUTURE.md](./FUTURE.md). - -## 2. Understand the public API - -Check `src/index.ts` — only re-exported symbols are public. Much of the tree -(MFA, OAuth, session, token) is in-tree but unpublished. - -## 3. Key patterns - -- **Domain errors:** `AuthCoreErrorRegistry` + `DomainError<'NS.CODE'>` -- **Application outcomes:** `Result` from `@rineex/ddd` (v5+ — use `Result.err`, - not `Result.fail`) -- **Auth methods:** implement `AuthMethodPort` (see OTP package) -- **Policy:** implement `AuthPolicyEvaluator`, register with `AuthPolicyEngine` - -## 4. Run locally - -```bash -cd packages/authentication/core -pnpm install # from monorepo root -pnpm test -pnpm lint -pnpm check-types -``` - -## 5. Where to add code - -| Change | Location | -| ------------------ | ----------------------------------------- | -| New auth method | `packages/authentication/methods//` | -| Core domain rule | `src/domain/` | -| New outbound port | `src/ports/outbound/` | -| Flow orchestration | `src/application/services/` | - -## 6. Common pitfalls - -- Do not assume Principal/Credential aggregates exist — use `Identity` entity -- Passwordless is not on `AuthMethodPort` yet -- Two types named `Identity` — check import path -- `ApplicationServicePort` returns `Promise`, not `Result` - -## 7. Next tasks - -See [TASK_BREAKDOWN.md](./TASK_BREAKDOWN.md) for grab-able work items. diff --git a/packages/authentication/core/README.md b/packages/authentication/core/README.md index f3d37b7..d513e00 100644 --- a/packages/authentication/core/README.md +++ b/packages/authentication/core/README.md @@ -1,68 +1,16 @@ # @rineex/auth-core -Core authentication package for the Rineex framework. Built on `@rineex/ddd` -with a plugin model for auth methods. - -## What Is Implemented - -| Area | Status | -| ---------------------------------------------- | --------------------------------------------------------- | -| Authentication attempt aggregate + events | Exported | -| Identity entity + value objects | Exported | -| Policy engine (`AuthPolicyEngine`) | Exported | -| `AuthMethodPort` + flow orchestration services | Port exported; services in-tree | -| OTP method adapter | `@rineex/authentication-method-otp` | -| Passwordless challenge domain | `@rineex/authentication-method-passwordless` (standalone) | -| MFA session domain + services | In-tree, not exported | -| OAuth authorization domain | In-tree, not exported | -| Session / token domains | In-tree, not exported | - -**Not implemented:** SSO, social login, credential lifecycle, Flow DSL, trust -levels. - -## Installation +Framework-independent authentication domain contracts. The package exports +identity aggregates/entities/value objects/events, policy contracts and engine, +inbound/outbound ports, and authentication types. ```bash pnpm add @rineex/auth-core @rineex/ddd ``` -## Documentation - -| Document | Description | -| ----------------------------------------------------- | ------------------------------------------ | -| [Architecture.md](./Architecture.md) | Domain model and layering (as implemented) | -| [Definition.md](./Definition.md) | Contracts, registries, ports | -| [RULES.md](./RULES.md) | Package topology and port map | -| [GAP_ANALYSIS.md](./GAP_ANALYSIS.md) | Remaining work | -| [ONBOARDING.md](./ONBOARDING.md) | Contributor guide | -| [Domain errors](./src/domain/errors/README.md) | Error classes and codes | -| [Value objects](./src/domain/value-objects/README.md) | VO reference | -| [FUTURE.md](./FUTURE.md) | Aspirational design (not implemented) | - -## Quick Example - -```typescript -import { - AuthenticationAttempt, - AuthAttemptId, - AuthMethodPort, -} from '@rineex/auth-core'; - -// Register an AuthMethodPort implementation (e.g. OtpAuthMethod) at composition root -// Wire AuthenticationAttemptRepositoryPort, IdentityRepository, DomainEventPublisherPort -``` - -See `@rineex/authentication-method-otp` for a complete method adapter. - -## Development - -```bash -pnpm build -pnpm test -pnpm lint -pnpm check-types -``` - -## License +Compose exported ports with application-specific repositories, session storage, +event publication, and method implementations. Internal application services, +OAuth/MFA code, and infrastructure are not public package exports. -Apache-2.0 +See [Authentication](../../../docs/authentication.md) for integration +boundaries. diff --git a/packages/authentication/core/ROADMAP.md b/packages/authentication/core/ROADMAP.md deleted file mode 100644 index cd26584..0000000 --- a/packages/authentication/core/ROADMAP.md +++ /dev/null @@ -1,38 +0,0 @@ -# Auth Core — Roadmap - -Phased plan from current state. Timelines are indicative. - -## Phase 1 — Integration consistency (current focus) - -- Export passwordless runtime API from package entry -- Add `PasswordlessAuthMethod` implementing `AuthMethodPort` -- Wire passwordless into flow orchestration services -- Implement email/SMS channel adapters (or document consumer responsibility) - -## Phase 2 — Publish in-tree domains - -- Export MFA session domain + services (or subpath `@rineex/auth-core/mfa`) -- Complete `OAuthAuthorizeService` and export OAuth domain -- Export session and token domains with clear stability tiers -- Resolve `Identity` entity vs aggregate naming - -## Phase 3 — Policy and risk - -- Expand `AuthPolicyRegistry` beyond `base` -- Populate `RiskSignalRegistry` and wire evaluators -- Step-up MFA flow end-to-end through policy engine - -## Phase 4 — Platform features (from FUTURE.md) - -- Flow DSL (data-driven authentication flows) -- Credential lifecycle -- Trust levels on sessions -- Additional methods: password, passkeys, social, API tokens - -## Completed (documentation sync) - -- Rewrote Architecture, Definition, RULES to match implementation -- Synced docs with `@rineex/ddd` v5/v6 (Result union, registry errors) -- Created FUTURE.md preserving aspirational design - -See [GAP_ANALYSIS.md](./GAP_ANALYSIS.md) for detailed gap list. diff --git a/packages/authentication/core/RULES.md b/packages/authentication/core/RULES.md deleted file mode 100644 index 282da9b..0000000 --- a/packages/authentication/core/RULES.md +++ /dev/null @@ -1,89 +0,0 @@ -# Auth Core — Package Topology and Port Map - -Actual layout as implemented. The `@auth/core-domain` / `@auth/core-ports` split -described in earlier drafts was never built — everything lives in -`@rineex/auth-core` with method packages alongside. - -## Package Topology - -``` -packages/authentication/ -├── core/ @rineex/auth-core -│ ├── domain/ identity, policy, mfa, oauth, session, token -│ ├── application/ flow + MFA services (unexported) -│ ├── ports/ inbound + outbound -│ └── types/ registries -└── methods/ - ├── otp/ @rineex/authentication-method-otp - └── passwordless/ @rineex/authentication-method-passwordless -``` - -No `adapters/`, `orchestration/`, or `policies/` packages exist at this level. -Framework adapters are consumer responsibilities. - -## Dependency Rules - -| Package | May depend on | -| -------------------------------------------- | ------------------------------------- | -| `@rineex/auth-core` | `@rineex/ddd` | -| `@rineex/authentication-method-otp` | `@rineex/auth-core`, `@rineex/ddd` | -| `@rineex/authentication-method-passwordless` | `@rineex/auth-core`, `@rineex/ddd` | -| Consumer apps | Any of the above + their own adapters | - -Method packages must not import each other. - -## Inbound Ports - -| Port | Path | Implemented by | -| ---------------------------- | ------------------------------------- | --------------- | -| `AuthMethodPort` | `ports/inbound/auth-method.port.ts` | `OtpAuthMethod` | -| `StartAuthenticationCommand` | `ports/inbound/start-auth.command.ts` | Consumer | - -## Outbound Ports (exported) - -| Port | Path | -| ------------------------------------- | ---------------------------------------------------------- | -| `AuthenticationAttemptRepositoryPort` | `ports/outbound/authentication-attempt.repository.port.ts` | -| `IdentityRepository` | `ports/outbound/identity.repository.outbound.port.ts` | -| `DomainEventPublisherPort` | `ports/outbound/domain-event-publisher.port.ts` | - -## Outbound Ports (in-tree) - -| Port | Path | -| ------------------------ | ------------------------------------------------- | -| `SessionRepositoryPort` | `ports/outbound/session.repository.port.ts` | -| `TokenRepository` | `ports/repositories/token.repository.ts` | -| `MfaSessionRepository` | `ports/mfa/mfa-session-repository.port.ts` | -| `MfaSessionIdGenerator` | `ports/mfa/mfa-session-id-generator.port.ts` | -| `MfaClock` | `ports/mfa/mfa-clock.port.ts` | -| `LoggerPort` | `ports/log/log.port.ts` | -| `ObservabilityEventPort` | `ports/observability/observability-event.port.ts` | - -## Passwordless Ports (passwordless package) - -| Port | Path | -| --------------------------------- | ---------------------------------------------- | -| `PasswordlessChallengeRepository` | `methods/passwordless/src/ports/repositories/` | -| `PasswordlessChannelPort` | `methods/passwordless/src/ports/channels/` | -| `PasswordlessIdGeneratorPort` | `methods/passwordless/src/ports/` | - -## OTP Ports - -| Port | Package | -| ---------------- | ---------------------------------------------- | -| `OtpChannelPort` | `@rineex/authentication-method-otp` (exported) | - -## Layer Rules - -1. Domain must not import application or infrastructure -2. Application orchestrates domain + ports -3. Ports are interfaces only — no implementations in core -4. Method packages implement inbound ports; consumers implement outbound ports -5. Domain throws `DomainError`; application services return `Result` (v5+ API) - -## Export Policy - -Only symbols re-exported from `core/src/index.ts` are public API. In-tree -domains (MFA, OAuth, session, token) are internal until explicitly exported. - -See [Architecture.md](./Architecture.md) for the full exported vs in-tree list. diff --git a/packages/authentication/core/TASK_BREAKDOWN.md b/packages/authentication/core/TASK_BREAKDOWN.md deleted file mode 100644 index 098b7f1..0000000 --- a/packages/authentication/core/TASK_BREAKDOWN.md +++ /dev/null @@ -1,42 +0,0 @@ -# Auth Core — Task Breakdown - -Granular tasks derived from [IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md). - -## Passwordless integration - -- [ ] Export passwordless services, aggregate, and ports from `index.ts` -- [ ] Add `PasswordlessAuthMethod` class implementing `AuthMethodPort` -- [ ] Wire `PasswordlessAuthMethod` into `AuthenticationMethodResolver` -- [ ] Integration test: start attempt → passwordless start → verify → succeed - -## Export hygiene - -- [ ] Add export tier table to README -- [ ] Decide MFA subpath export vs main entry promotion -- [ ] Export `AuthCoreErrorRegistry` from package entry -- [ ] Resolve Identity entity vs aggregate naming collision - -## OAuth - -- [ ] Implement `OAuthAuthorizeService` with repository dependency -- [ ] Export OAuth authorization aggregate and value objects -- [ ] Add OAuth flow integration test (domain level) - -## Error consistency - -- [ ] Replace plain `Error` in `Token` aggregate with `EntityValidationError` -- [ ] Add domain error for invalid `OtpCode` in core VO -- [ ] Verify all error classes in architecture spec - -## Channels - -- [ ] Implement or remove `EmailChannel` stub -- [ ] Implement or remove `SMSChannelImp` stub -- [ ] Document channel adapter contract in passwordless README - -## Documentation (completed in sync) - -- [x] Rewrite Architecture.md, Definition.md, RULES.md -- [x] Update domain errors and value objects READMEs -- [x] Sync Result/DomainError patterns across packages -- [x] Preserve aspirational spec in FUTURE.md diff --git a/packages/authentication/core/src/domain/errors/README.md b/packages/authentication/core/src/domain/errors/README.md deleted file mode 100644 index 1e2f402..0000000 --- a/packages/authentication/core/src/domain/errors/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# Authentication Domain Errors - -Domain error classes for `@rineex/auth-core`. All extend `DomainError` from -`@rineex/ddd` using the v6 registry-backed, code-first API. - -## Error Structure - -Every domain error has: - -- **code**: Machine-readable `NAMESPACE.ERROR_NAME` literal (e.g. - `AUTH_CORE_TOKEN.INVALID`) -- **message**: Human-readable description -- **metadata**: Optional primitive context (`Metadata` from `@rineex/ddd`) - -There is **no `type` field** — removed in `@rineex/ddd` v6. Use `code` for -classification. - -## Registry Pattern - -```typescript -import { InferErrorCodes } from '@rineex/ddd'; - -export const AuthCoreErrorRegistry = { - AUTH_CORE_TOKEN: ['INVALID'], - // ... other namespaces -} as const; - -export type AuthCoreDomainErrorCode = InferErrorCodes< - typeof AuthCoreErrorRegistry ->; -``` - -Architecture tests in `__tests__/auth-core-error.architecture.spec.ts` verify -every error class code is registered. - -## Namespaces - -### AUTH_CORE_TOKEN - -| Code | Class | Metadata | -| --------- | ----------------------- | --------------------------- | -| `INVALID` | `InvalidAuthTokenError` | `actualLength`, `minLength` | - -### AUTH_CORE_SESSION - -| Code | Class | -| --------- | --------------------- | -| `INVALID` | `InvalidSessionError` | - -### AUTH_CORE_SCOPE - -| Code | Class | -| --------- | ------------------- | -| `INVALID` | `InvalidScopeError` | - -### AUTH_CORE_IDENTITY - -| Code | Class | -| -------------------- | -------------------------------------- | -| `DISABLED_ERROR` | `IdentityDisabledError` | -| `INVALID_TRANSITION` | `InvalidAuthenticationTransitionError` | - -### AUTH_CORE_ATTEMPT - -| Code | Class | -| ----------------------- | ------------------------------- | -| `AUTHENTICATION_FAILED` | `AuthenticationAttemptError` | -| `NOT_FOUND` | `AuthenticationAttemptNotFound` | - -### AUTH_CORE_MFA - -| Code | Class | -| -------------------------- | ------------------------------- | -| `CHALLENGE_ID_INVALID` | MFA challenge ID errors | -| `CHALLENGE_STATUS_INVALID` | Invalid MFA challenge status | -| `CHALLENGE_EXPIRED` | `MfaChallengeExpiredError` | -| `EXPIRED` | `MfaExpiredError` | -| `ALREADY_VERIFIED` | `MfaAlreadyVerifiedError` | -| `ACTIVE_CHALLENGE_EXISTS` | `MfaActiveChallengeExistsError` | -| `ATTEMPTS_EXCEEDED` | `MfaAttemptsExceededError` | -| `SESSION_ID_INVALID` | MFA session ID errors | - -### AUTH_CORE_OAUTH - -| Code | Class | -| ---------------------------- | ------------------------------- | -| `INVALID_PROVIDER` | `InvalidOauthProviderError` | -| `INVALID_REDIRECT_URI` | `InvalidRedirectUriError` | -| `AUTHORIZATION_ALREADY_USED` | `AuthorizationAlreadyUsedError` | -| `INVALID_PKCE` | `InvalidPkceError` | -| `INVALID_AUTHORIZATION_CODE` | `InvalidAuthorizationCodeError` | -| `CONSENT_REQUIRED` | `ConsentRequiredError` | -| `AUTHORIZATION_EXPIRED` | `AuthorizationExpiredError` | - -## Usage - -```typescript -throw InvalidAuthTokenError.create('Authentication token is invalid', { - actualLength: 20, - minLength: 32, -}); - -throw InvalidSessionError.create(); -``` - -## Serialization - -`toObject()` returns `{ code, message, metadata }` — no `type` field. - -## Related - -- Registry: `auth-core-error.registry.ts` -- Base class: `auth-domain.error.ts` (optional base; most errors extend - `DomainError` directly) -- Passwordless errors: separate `PasswordlessErrorRegistry` in passwordless - package -- OTP errors: `OtpErrorRegistry` in OTP package - (`AUTH_OTP.AUTHENTICATION_FAILED`) diff --git a/packages/authentication/core/src/domain/value-objects/README.md b/packages/authentication/core/src/domain/value-objects/README.md deleted file mode 100644 index 487366a..0000000 --- a/packages/authentication/core/src/domain/value-objects/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# Authentication Domain Value Objects - -Value objects in `@rineex/auth-core`. Immutable, validated domain concepts. - -## Exported (public API) - -Available from `@rineex/auth-core` via `src/index.ts`: - -| VO | Description | -| ------------------ | -------------------------------------------------- | -| `AuthAttemptId` | UUID identifier for authentication attempts | -| `AuthFactor` | Auth factor name (registry: `password`) | -| `AuthMethod` | Auth method name (registry: `passwordless`, `otp`) | -| `AuthPolicy` | Policy name (registry: `base`) | -| `AuthStatus` | Attempt status: `pending`, `succeed`, `failed` | -| `IdentityId` | UUID identifier for identities | -| `IdentityProvider` | Provider name (registry currently empty) | -| `RiskSignal` | Risk signal name (registry currently empty) | - -### Examples - -```typescript -import { AuthAttemptId, AuthStatus, IdentityId } from '@rineex/auth-core'; - -const attemptId = AuthAttemptId.generate(); -const status = AuthStatus.pending(); -const identityId = IdentityId.fromString( - '550e8400-e29b-41d4-a716-446655440000', -); -``` - -## In-tree (not exported) - -### Identity - -- `IdentityStatus` — used by unexported Identity aggregate - -### MFA - -| VO | Description | -| -------------------- | ------------------------------------------ | -| `MfaChallengeId` | MFA challenge UUID | -| `MfaSessionId` | MFA session UUID | -| `MfaChallengeStatus` | `pending`, `verified`, `expired`, `failed` | - -### OAuth - -| VO | Description | -| --------------------------------------- | ------------------------ | -| `OauthAuthorizationId` | OAuth authorization UUID | -| `AuthorizationCodeId` | Authorization code ID | -| `AuthorizationCode` | OAuth authorization code | -| `ClientId` | OAuth client identifier | -| `CodeChallenge` / `CodeChallengeMethod` | PKCE | -| `Pkce` | PKCE bundle | -| `OauthProvider` | Provider name | -| `RedirectUri` | Redirect URI | -| `Scope` / `ScopeSet` | OAuth scopes | - -### Session - -| VO | Description | -| ----------- | ------------ | -| `SessionId` | Session UUID | - -### Token - -| VO | Description | -| -------------- | ----------------------------------------------- | -| `AuthToken` | Abstract cryptographic token (not JWT-specific) | -| `SessionToken` | Session-scoped token | - -## MFA Value Object Details - -### MfaChallengeStatus - -```typescript -const status = MfaChallengeStatus.pending(); -const verified = MfaChallengeStatus.create('verified'); -``` - -Throws `InvalidMfaChallengeStatusError` for invalid values. - -### AuthToken - -Abstract base — concrete formats (JWT, opaque) live in consumer adapters. -Minimum length: 32 characters. Throws `InvalidAuthTokenError`. - -## Guidelines - -1. **Immutability** — never mutate VO values after construction -2. **Equality** — use `equals()`, not `===` (except primitives inside) -3. **Validation** — happens at construction; throws `DomainError` or - `InvalidValueObjectError` -4. **Serialization** — use `.value` or `toString()` - -## Known gaps - -- `OtpCode` in core throws generic `Error` — migrate to domain error (see - GAP_ANALYSIS.md) -- `IdentityStatus` not exported despite use in Identity aggregate diff --git a/packages/authentication/methods/otp/README.md b/packages/authentication/methods/otp/README.md index 03e218f..2c265e5 100644 --- a/packages/authentication/methods/otp/README.md +++ b/packages/authentication/methods/otp/README.md @@ -1,89 +1,14 @@ # @rineex/authentication-method-otp -OTP authentication method for the Rineex auth system. Implements -`AuthMethodPort` from `@rineex/auth-core` and delegates delivery/verification to -`OtpChannelPort`. - -## Installation +An OTP implementation of the `@rineex/auth-core` authentication-method port. ```bash -pnpm add @rineex/authentication-method-otp @rineex/auth-core @rineex/ddd +pnpm add @rineex/authentication-method-otp @rineex/auth-core ``` -## Public API - -| Export | Description | -| ---------------- | ------------------------------- | -| `OtpAuthMethod` | `AuthMethodPort` implementation | -| `OtpChannelPort` | Delivery and verification seam | - -## Usage - -```typescript -import { - OtpAuthMethod, - OtpChannelPort, -} from '@rineex/authentication-method-otp'; -import { OtpCode } from './otp-code'; // internal VO; provide generator - -const channel: OtpChannelPort = { - async sendOtp(identityId, otp) { - // deliver via SMS, email, etc. - }, - async verifyOtp(identityId, otp) { - return true; - }, -}; - -const otpMethod = new OtpAuthMethod(channel, () => OtpCode.generate()); - -// Register with flow orchestration at composition root -const outcome = await otpMethod.start({ - authAttemptId, - ctx: { identityId: identityId.value }, -}); -``` - -## AuthMethodPort contract - -- `start` — generates OTP via `otpGenerator`, sends via `OtpChannelPort` -- `verify` — parses payload, verifies via `OtpChannelPort` - -Returns `AuthMethodOutcome`: `{ ok: true }` or `{ ok: false, violation }`. - -This is **not** `Result` — the application layer wraps outcomes as needed. - -## Error registry - -```typescript -export const OtpErrorRegistry = { - AUTH_OTP: ['AUTHENTICATION_FAILED'], -} as const; -``` - -`OtpAuthenticationError` uses code `AUTH_OTP.AUTHENTICATION_FAILED`. - -## Integration with auth-core - -1. Implement `OtpChannelPort` in your infrastructure layer -2. Instantiate `OtpAuthMethod` with channel + generator -3. Register with `AuthenticationMethodResolver` (consumer wiring) -4. Wire `AuthenticationAttemptRepositoryPort` and flow services - -## Development - -```bash -cd packages/authentication/methods/otp -pnpm test -pnpm lint -pnpm check-types -``` - -## Related - -- [@rineex/auth-core](../../core/README.md) -- [Passwordless method](../passwordless/README.md) — standalone challenge flow - -## License +Construct `OtpAuthMethod` with an `OtpChannelPort` and an OTP generator. The +channel delivers and verifies `OtpCode` values for an `IdentityId`; it may be +backed by SMS, email, push, voice, or another transport. The package exports +`OtpAuthMethod` and `OtpChannelPort`. -Apache-2.0 +See [Authentication](../../../../docs/authentication.md) for input requirements. diff --git a/packages/authentication/methods/passwordless/DOCS.md b/packages/authentication/methods/passwordless/DOCS.md deleted file mode 100644 index f8e4be8..0000000 --- a/packages/authentication/methods/passwordless/DOCS.md +++ /dev/null @@ -1,5 +0,0 @@ -# Passwordless — Documentation - -See [README.md](./README.md) for the complete guide. - -This file is kept as a pointer to avoid maintaining duplicate documentation. diff --git a/packages/authentication/methods/passwordless/High-level-Architrcture.md b/packages/authentication/methods/passwordless/High-level-Architrcture.md deleted file mode 100644 index f8b3f64..0000000 --- a/packages/authentication/methods/passwordless/High-level-Architrcture.md +++ /dev/null @@ -1,45 +0,0 @@ -# Passwordless — High-Level Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Application Service Layer │ -├─────────────────────────────────────────────────────────────┤ -│ IssuePasswordlessChallengeService │ -│ VerifyPasswordlessChallengeService │ -│ • Orchestrate aggregate │ -│ • Coordinate ports │ -│ • Return Result (v5 API: Result.err, not Result.fail) │ -└──────────────────────────┬──────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ PasswordlessChallengeAggregate │ -│ issue() · verify() · isExpired() · matchesSecret() │ -│ Events: Issued · Verified │ -└──────────────────────────┬──────────────────────────────────┘ - │ - ┌─────────────────┴─────────────────┐ - ▼ ▼ -┌─────────────────────┐ ┌─────────────────────┐ -│ Ports │ │ Value Objects │ -├─────────────────────┤ ├─────────────────────┤ -│ PasswordlessChallenge│ │ PasswordlessChallengeId│ -│ Repository │ │ PasswordlessChannel │ -│ PasswordlessId │ │ ChallengeDestination │ -│ GeneratorPort │ │ ChallengeSecret │ -│ PasswordlessChannel │ │ PasswordlessChallenge │ -│ Port │ │ Status │ -│ ClockPort (@rineex/ │ │ OtpCode │ -│ ddd) │ └─────────────────────┘ -└──────────┬──────────┘ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Infrastructure (consumer-provided) │ -│ DB repository · Clock adapter · Email/SMS/Push channels │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Integration note - -Passwordless is **not** on `AuthMethodPort` yet. OTP uses the port; passwordless -uses standalone services. See `@rineex/auth-core` GAP_ANALYSIS.md. diff --git a/packages/authentication/methods/passwordless/README.md b/packages/authentication/methods/passwordless/README.md index 758f30f..b63a9ff 100644 --- a/packages/authentication/methods/passwordless/README.md +++ b/packages/authentication/methods/passwordless/README.md @@ -1,513 +1,16 @@ # @rineex/authentication-method-passwordless -Passwordless authentication method for the Rineex authentication system. -Implements a DDD-based challenge-response flow supporting multiple delivery -channels (email, SMS, push notifications, authenticator apps). - -## Overview - -### What This Component Does - -- Manages the lifecycle of passwordless authentication challenges (OTP codes, - push nonces, etc.) -- Provides a channel-agnostic architecture for delivering secrets via email, - SMS, push, or authenticator apps -- Emits domain events for challenge issuance and verification -- Uses timing-safe secret comparison to prevent timing attacks - -### When to Use - -- Implementing passwordless login flows (magic links, OTP codes) -- Building multi-factor authentication with OTP verification -- Creating authentication systems that don't rely on passwords - -### When NOT to Use - -- Password-based authentication (use a separate auth method) -- Session management (this only handles challenge verification, not session - creation) -- Direct integration without implementing required ports (repository, channel, - ID generator) - -## Installation +Passwordless challenge domain and application code built on `@rineex/auth-core` +and `@rineex/ddd`. ```bash pnpm add @rineex/authentication-method-passwordless ``` -## Architecture - -The package follows Domain-Driven Design (DDD) principles with clear separation -between domain, application, and infrastructure layers. - -### Layer Structure - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Application Service Layer │ -├─────────────────────────────────────────────────────────────┤ -│ IssuePasswordlessChallengeService │ -│ ───────────────────────────────────── │ -│ • Orchestrates aggregate creation │ -│ • Coordinates ports (repository, ID generator, clock) │ -│ • Returns Result │ -│ │ -│ VerifyPasswordlessChallengeService │ -│ ───────────────────────────────────── │ -│ • Loads challenge from repository │ -│ • Delegates verification to aggregate │ -│ • Persists state changes │ -│ • Returns Result │ -└───────────────────────┬─────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ PasswordlessChallengeAggregate │ -│ (Domain Layer) │ -├─────────────────────────────────────────────────────────────┤ -│ Properties: │ -│ • id: PasswordlessChallengeId │ -│ • channel: PasswordlessChannel │ -│ • destination: ChallengeDestination │ -│ • secret: ChallengeSecret │ -│ • status: PasswordlessChallengeStatus │ -│ • issuedAt: Date │ -│ • expiresAt: Date │ -│ │ -│ Methods: │ -│ • issue() - Factory method, emits IssuedEvent │ -│ • verify() - Validates secret, emits VerifiedEvent │ -│ • isExpired() - Checks expiration │ -│ • matchesSecret() - Timing-safe comparison │ -│ • validate() - Enforces invariants │ -│ │ -│ Domain Events: │ -│ • PasswordlessChallengeIssuedEvent │ -│ • PasswordlessChallengeVerifiedEvent │ -└───────────────────────┬─────────────────────────────────────┘ - │ - ┌───────────────┴───────────────┐ - │ │ - ▼ ▼ -┌───────────────────────┐ ┌──────────────────────────┐ -│ Ports (Interfaces) │ │ Value Objects │ -├───────────────────────┤ ├──────────────────────────┤ -│ • PasswordlessChallenge│ │ • PasswordlessChallengeId│ -│ Repository │ │ • PasswordlessChannel │ -│ • PasswordlessId │ │ • ChallengeDestination │ -│ GeneratorPort │ │ • ChallengeSecret │ -│ • ClockPort │ │ • PasswordlessChallenge │ -│ • PasswordlessChannel │ │ Status │ -│ Port │ │ • OtpCode │ -└───────────────────────┘ └──────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Infrastructure Layer │ -│ (Implementations - Not Included) │ -├─────────────────────────────────────────────────────────────┤ -│ • Database Repository Implementation │ -│ • Clock Adapter (system time, test doubles) │ -│ • Channel Adapters: │ -│ - Email Channel (SMTP, SendGrid, etc.) │ -│ - SMS Channel (Twilio, AWS SNS, etc.) │ -│ - Push Notification Channel │ -│ - Authenticator App Channel │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Design Principles - -- **Port-Adapter Pattern**: Domain depends on abstractions (ports), not - implementations -- **Aggregate Root**: `PasswordlessChallengeAggregate` encapsulates challenge - lifecycle and business rules -- **Domain Events**: State changes emit events for downstream processing -- **Result Pattern**: Application services return `Result` for explicit - error handling -- **Value Objects**: Immutable, validated domain concepts (Channel, Secret, - Destination, etc.) - -### Flow: Issuing a Challenge - -1. Application service receives request with channel, destination, and secret -2. Service uses `PasswordlessIdGeneratorPort` to generate unique challenge ID -3. Service uses `ClockPort` to get current timestamp -4. Service calls `PasswordlessChallengeAggregate.issue()` factory method -5. Aggregate validates invariants and emits `PasswordlessChallengeIssuedEvent` -6. Service persists aggregate via `PasswordlessChallengeRepository` -7. Service returns `Result.ok(challenge)` or `Result.err(error)` - -### Flow: Verifying a Challenge - -1. Application service receives challenge ID and secret -2. Service loads challenge from repository via `findById()` -3. Service checks if challenge exists (returns `Result.err` if not found) -4. Service checks if challenge is expired (returns `Result.err` if expired) -5. Service calls `challenge.verify(secret)` on aggregate -6. Aggregate validates secret using timing-safe comparison -7. Aggregate updates status to `verified` and emits - `PasswordlessChallengeVerifiedEvent` -8. Service persists updated aggregate -9. Service returns `Result.ok(challenge)` or `Result.err(error)` - -## Public API - -> **Export gap:** The runtime API described below exists in source but is **not -> exported** from `src/index.ts` today — the package entry only loads type -> augmentation for `AuthMethodRegistry`. Import from source paths in the -> monorepo or wait until exports are added (see auth-core GAP_ANALYSIS.md). - -### PasswordlessChallengeAggregate - -The aggregate root managing passwordless challenge state and behavior. - -#### Static Methods - -| Method | Parameters | Returns | Description | -| --------- | ------------------------- | -------------------------------- | ------------------------------------------------------------------------------- | -| `issue()` | `CreatePasswordlessProps` | `PasswordlessChallengeAggregate` | Creates and issues a new challenge, emitting `PasswordlessChallengeIssuedEvent` | - -#### Instance Methods - -| Method | Parameters | Returns | Description | -| ----------------- | ------------------------------ | --------- | --------------------------------------------------------------- | -| `verify()` | `secret: string`, `now?: Date` | `void` | Verifies the challenge with provided secret. Throws on failure. | -| `isExpired()` | `now?: Date` | `boolean` | Checks if challenge has expired | -| `matchesSecret()` | `input: string` | `boolean` | Timing-safe secret comparison | -| `toObject()` | - | `object` | Returns plain object representation | -| `validate()` | - | `void` | Validates aggregate invariants | - -#### Errors Thrown by `verify()` - -| Error | Condition | -| ------------------------------------------ | ------------------------------------- | -| `PasswordlessChallengeExpiredError` | Challenge has passed expiration time | -| `PasswordlessChallengeAlreadyUsedError` | Challenge already verified or expired | -| `PasswordlessChallengeSecretMismatchError` | Provided secret doesn't match | - -### IssuePasswordlessChallengeService - -Application service for issuing new passwordless challenges. - -#### Constructor - -```typescript -constructor( - repository: PasswordlessChallengeRepository, - idGenerator: PasswordlessIdGeneratorPort, - clock: ClockPort -) -``` - -#### Methods - -| Method | Parameters | Returns | Description | -| ----------- | ---------- | -------------------------------------------------------- | ------------------------------------ | -| `execute()` | `Input` | `Promise>` | Creates and persists a new challenge | - -#### Input Type - -```typescript -type Input = { - channel: PasswordlessChannel; - destination: ChallengeDestination; - secret: ChallengeSecret; - ttlSeconds?: ms.StringValue; // Default: '300s' (5 minutes) -}; -``` - -### VerifyPasswordlessChallengeService - -Application service for verifying passwordless challenges. - -#### Constructor - -```typescript -constructor(repository: PasswordlessChallengeRepository) -``` - -#### Methods - -| Method | Parameters | Returns | Description | -| ----------- | ---------- | -------------------------------------------------------- | ---------------------------------------------------------------- | -| `execute()` | `Input` | `Promise>` | Verifies a challenge with the provided secret and persists state | - -#### Input Type - -```typescript -type Input = { - id: PasswordlessChallengeId; - secret: ChallengeSecret; -}; -``` - -#### Errors Returned - -| Error | Condition | -| ------------------------------------------ | ------------------------------------ | -| `PasswordlessChallengeNotFoundError` | Challenge ID not found in repository | -| `PasswordlessChallengeExpiredError` | Challenge has expired | -| `PasswordlessChallengeSecretMismatchError` | Provided secret doesn't match | - -Any other error thrown during execution (e.g. -`PasswordlessChallengeAlreadyUsedError` from the aggregate, or repository -rejections) is caught and returned as `Result.err(error)`. - -### Value Objects - -| Class | Factory Method | Validation | -| ----------------------------- | ------------------------------------- | -------------------------------------------------------------- | -| `PasswordlessChannel` | `create(channel)` | Must be `'email'`, `'sms'`, `'authenticator_app'`, or `'push'` | -| `ChallengeDestination` | `create(value)` | Minimum 3 characters | -| `ChallengeSecret` | `create(value)` | Minimum 4 characters | -| `OtpCode` | `create(value)` | Exactly 6 digits | -| `PasswordlessChallengeId` | `fromString(id)` | Valid domain ID | -| `PasswordlessChallengeStatus` | `issued()`, `verified()`, `expired()` | Enum-like, closed union | - -### Domain Events - -| Event | Event Name | Payload | -| ------------------------------------ | -------------------------------------- | -------------------------------------- | -| `PasswordlessChallengeIssuedEvent` | `auth.passwordless.challenge_created` | `{ channel, destination, expiresAt }` | -| `PasswordlessChallengeVerifiedEvent` | `auth.passwordless.challenge_verified` | `{ channel, destination, verifiedAt }` | - -### Ports (Interfaces to Implement) - -#### PasswordlessChallengeRepository - -```typescript -type PasswordlessChallengeRepository = { - save: (challenge: PasswordlessChallengeAggregate) => Promise; - findById: (id: string) => Promise; -}; -``` - -#### PasswordlessChannelPort - -```typescript -type PasswordlessChannelPort = { - readonly channelName: PasswordlessChannel; - deliver: ( - destination: ChallengeDestination, - secret: ChallengeSecret, - ) => Promise; -}; -``` - -#### PasswordlessIdGeneratorPort - -```typescript -type PasswordlessIdGeneratorPort = { - generate: () => PasswordlessChallengeId; -}; -``` - -### PasswordlessChannelRegistry - -Runtime registry for channel implementations. Use `init()` to create; channels -are keyed by channel name. - -```typescript -class PasswordlessChannelRegistry { - static init( - channels: readonly PasswordlessChannelPort[], - ): PasswordlessChannelRegistry; - resolve(key: string): PasswordlessChannelPort; // throws if channel not registered - supports(channel: PasswordlessChannel): boolean; -} -``` - -## Usage Examples - -### Issuing a Challenge - -```typescript -import { - IssuePasswordlessChallengeService, - PasswordlessChannel, - ChallengeDestination, - ChallengeSecret, -} from '@rineex/authentication-method-passwordless'; - -const service = new IssuePasswordlessChallengeService( - challengeRepository, - idGenerator, - clock, -); - -const result = await service.execute({ - channel: PasswordlessChannel.create('email'), - destination: ChallengeDestination.create('user@example.com'), - secret: ChallengeSecret.create('123456'), - ttlSeconds: '5m', -}); - -if (result.isOk()) { - const challenge = result.value; - // Deliver secret via channel -} -``` - -### Verifying a Challenge - -Using the application service (recommended): - -```typescript -import { - VerifyPasswordlessChallengeService, - PasswordlessChallengeId, - ChallengeSecret, -} from '@rineex/authentication-method-passwordless'; - -const verifyService = new VerifyPasswordlessChallengeService(repository); - -const result = await verifyService.execute({ - id: PasswordlessChallengeId.fromString(challengeId), - secret: ChallengeSecret.create(userProvidedCode), -}); - -if (result.isOk()) { - const challenge = result.value; - // Challenge verified - create session -} else { - // Handle error (expired, not found, secret mismatch) - const error = result.error; -} -``` - -Direct aggregate usage (if you need more control): - -```typescript -import { - PasswordlessChallengeExpiredError, - PasswordlessChallengeSecretMismatchError, - PasswordlessChallengeAlreadyUsedError, -} from '@rineex/authentication-method-passwordless'; - -const challenge = await repository.findById(challengeId); - -if (!challenge) { - // Handle not found - return; -} - -try { - challenge.verify(userProvidedCode); - // Challenge verified - create session - await repository.save(challenge); -} catch (error) { - if (error instanceof PasswordlessChallengeExpiredError) { - // Handle expired challenge - } - if (error instanceof PasswordlessChallengeAlreadyUsedError) { - // Handle already used challenge - } - if (error instanceof PasswordlessChallengeSecretMismatchError) { - // Handle invalid code - } -} -``` - -### Implementing a Channel - -```typescript -import { - PasswordlessChannelPort, - PasswordlessChannel, - ChallengeDestination, - ChallengeSecret, -} from '@rineex/authentication-method-passwordless'; - -const emailChannel: PasswordlessChannelPort = { - channelName: PasswordlessChannel.create('email'), - deliver: async ( - destination: ChallengeDestination, - secret: ChallengeSecret, - ) => { - await emailService.send({ - to: destination.value, - subject: 'Your verification code', - body: `Your code is: ${secret.value}`, - }); - }, -}; - -channelRegistry.register(emailChannel); -``` - -### Extending Channel Types (TypeScript) - -```typescript -declare module '@rineex/authentication-method-passwordless' { - interface PasswordlessChannelRegistry { - readonly custom_channel: true; - } -} -``` - -## Behavior & Guarantees - -### Invariants - -- A challenge cannot be verified after expiration -- A challenge can only be verified once (single-use) -- Secret comparison is timing-safe (prevents timing attacks) -- Expiration time must be after issuance time -- Channel and secret are required properties - -### Security Characteristics - -- Uses SHA-256 hashing with `crypto.timingSafeEqual` for secret comparison -- Secrets are never logged in domain events (only destination and channel) -- Challenges are single-use to prevent replay attacks - -### Status State Machine - -``` -issued ──verify()──> verified (final) - │ - └──expires──> expired (final) -``` - -## Operational Notes - -### Configuration - -| Parameter | Default | Description | -| ------------ | -------- | ------------------------------------------------------------------------- | -| `ttlSeconds` | `'300s'` | Challenge validity duration (accepts `ms` format: `'5m'`, `'300s'`, etc.) | - -### Domain Error Codes - -| Code | Type | Description | -| ------------------------------------------ | ---------------------- | -------------------------- | -| `AUTH_PASSWORDLESS.CHALLENGE_EXPIRED` | `DOMAIN.INVALID_STATE` | Challenge has expired | -| `AUTH_PASSWORDLESS.CHALLENGE_ALREADY_USED` | `DOMAIN.INVALID_STATE` | Challenge already verified | -| `AUTH_PASSWORDLESS.CHALLENGE_NOT_FOUND` | `DOMAIN.INVALID_STATE` | Challenge not found | -| `AUTH_PASSWORDLESS.SECRET_MISMATCH` | `DOMAIN.INVALID_VALUE` | Secret doesn't match | -| `AUTH_PASSWORDLESS.CHANNEL_REQUIRED` | `DOMAIN.INVALID_VALUE` | Missing channel | -| `AUTH_PASSWORDLESS.SECRET_REQUIRED` | `DOMAIN.INVALID_VALUE` | Missing secret | -| `AUTH_PASSWORDLESS.INVALID_EXPIRATION` | `DOMAIN.INVALID_VALUE` | Invalid expiration time | - -### Common Pitfalls - -- **Not persisting after verification**: Always save the challenge after calling - `verify()` to persist the status change -- **Reusing challenges**: Challenges are single-use; issue a new one for each - authentication attempt -- **Missing channel implementation**: Register channel implementations before - use via `PasswordlessChannelRegistry` -- **Clock skew**: Use consistent clock implementations across services to - prevent premature expiration - -### Dependencies - -- `@rineex/auth-core`: Core authentication abstractions -- `@rineex/ddd`: Domain-driven design primitives (AggregateRoot, ValueObject, - DomainEvent, Result) -- `ms`: Time string parsing - -## License +At this revision, the package entry point exports no runtime or type API. Its +challenge aggregate, services, registries, ports, and email/SMS channels are +internal source modules. Avoid deep imports until a public entry point is +intentionally defined. -Apache-2.0 +See [Authentication](../../../../docs/authentication.md) for the supported +boundary. diff --git a/packages/ddd/README.md b/packages/ddd/README.md index 577611a..4ee7b4d 100644 --- a/packages/ddd/README.md +++ b/packages/ddd/README.md @@ -1,850 +1,16 @@ # @rineex/ddd -> Domain-Driven Design (DDD) primitives for building maintainable, scalable -> TypeScript applications. - -[![npm version](https://img.shields.io/npm/v/@rineex/ddd)](https://www.npmjs.com/package/@rineex/ddd) -[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue.svg)](https://www.typescriptlang.org/) - -## Table of Contents - -- [Overview](#overview) -- [Installation](#installation) -- [Package Exports](#package-exports) - - [Value Objects](#value-objects) -- [Primitive Value Objects](#primitive-value-objects) - - [Entities](#entities) - - [Aggregate Roots](#aggregate-roots) - - [Domain Events](#domain-events) -- [Domain Errors](#domain-errors) -- [Result Type](#result-type) - - [Application Services](#application-services) -- [Ports & Utilities](#ports--utilities) -- [Integration Guide](#integration-guide) -- [API Reference](#api-reference) -- [License](#license) - ---- - -## Overview - -`@rineex/ddd` provides type-safe building blocks for implementing Domain-Driven -Design patterns. Used by `@rineex/auth-core` and other Rineex packages. - -**Features:** Value Objects, Entities, Aggregate Roots, Domain Events, Domain -Errors (extensible namespaces), Result type, Application Service port, Clock -port, HTTP status constants. - -For placement rules, aggregate design, event recording, mapper-based -rehydration, and testing guidance, see [the DDD guide](./docs/DDD-GUIDE.md). For -the breaking API migration, see [MIGRATION.md](./docs/MIGRATION.md). - ---- - -## Installation +Domain-driven design primitives for TypeScript applications: value objects, +entities, aggregates, domain events, results, errors, ports, mappers, and +utility types. ```bash pnpm add @rineex/ddd ``` -**Requirements:** Node.js 18+, TypeScript 5.0+, ES2020+ target - ---- - -## Package Exports - -```typescript -import { - ValueObject, - PrimitiveValueObject, - Entity, - AggregateRoot, - DomainEvent, - AggregateId, - DomainID, - Email, - IPAddress, - DomainError, - InferErrorCodes, - registryErrorCodes, - CoreDomainErrorRegistry, - BaseMapper, - InvalidValueObjectError, - EntityValidationError, - InvalidValueError, - InvalidStateError, - InternalError, - TimeoutError, - ApplicationError, - ApplicationServicePort, - Result, - ClockPort, - EntityId, - EntityProps, - DomainEventPayload, - CreateEventProps, - UnixTimestampMillis, - HttpStatus, - HttpStatusMessage, - deepFreeze, -} from '@rineex/ddd'; -``` - ---- - -## Value Objects - -Value objects are immutable and defined by attributes. Use `ValueObject` for -composite structures. Props are deep-frozen in the constructor. - -### Example (from `vo.spec.ts`) - -```typescript -import { ValueObject, InvalidValueObjectError } from '@rineex/ddd'; - -class TestValueObject extends ValueObject<{ name: string; age: number }> { - constructor(props: { name: string; age: number }) { - super(props); - } - - protected validate(props: { name: string; age: number }): void { - if (!props.name?.trim()) { - throw InvalidValueObjectError.create('Name is required'); - } - if (props.age < 0 || props.age > 150) { - throw InvalidValueObjectError.create('Age must be between 0 and 150'); - } - } -} - -// Usage -const vo = new TestValueObject({ name: 'John', age: 30 }); -vo.value; // { name: 'John', age: 30 } -vo.equals(other); // deep equality -vo.toJSON(); // returns props -vo.toString(); // JSON.stringify(props) -ValueObject.is(vo); // type guard -``` - -### Simple Value Object (wraps a single value) - -```typescript -class SimpleValueObject extends ValueObject { - constructor(value: string) { - super(value); - } - - protected validate(value: string): void { - if (!value?.length) { - throw InvalidValueObjectError.create('Value cannot be empty'); - } - } -} -``` - ---- - -## Primitive Value Objects - -For single primitives (string, number, boolean), extend -`PrimitiveValueObject`. Equality is by reference (`===`). - -Use `PrimitiveValueObject` for any validated primitive domain concept. Only -string, number, and bigint wrappers are valid entity IDs; a boolean wrapper is -not an identity type. - -### Example (from `primitive-vo.spec.ts`) - -```typescript -import { PrimitiveValueObject, InvalidValueObjectError } from '@rineex/ddd'; - -class StringVO extends PrimitiveValueObject { - constructor(value: string) { - super(value); - } - - protected validate(value: string): void { - if (!value?.length) { - throw InvalidValueObjectError.create('String cannot be empty'); - } - } -} - -class NumberVO extends PrimitiveValueObject { - constructor(value: number) { - super(value); - } - - protected validate(value: number): void { - if (value < 0) { - throw InvalidValueObjectError.create('Number must be non-negative'); - } - } -} - -// Usage -const s = new StringVO('test'); -s.value; // 'test' -s.getValue(); // deprecated, use .value -s.toString(); // 'test' -s.equals(new StringVO('test')); // true -``` - -### Pre-built: Email - -```typescript -import { Email } from '@rineex/ddd'; - -const email = Email.fromString('user@example.com'); -// or: new Email('user@example.com') -email.value; // 'user@example.com' -email.toString(); -``` - -### Pre-built: IPAddress - -```typescript -import { IPAddress } from '@rineex/ddd'; - -const ip = IPAddress.fromString('192.168.1.1'); -ip.value; // '192.168.1.1' -``` - -### Pre-built: AggregateId & DomainID - -```typescript -import { AggregateId, DomainID } from '@rineex/ddd'; - -// AggregateId -const id = AggregateId.generate(); -const fromStr = AggregateId.fromString('550e8400-e29b-41d4-a716-446655440000'); - -// DomainID – extend for custom IDs -class AuthAttemptId extends DomainID {} - -const attemptId = AuthAttemptId.generate(); -const parsed = AuthAttemptId.fromString('550e8400-e29b-41d4-a716-446655440000'); -``` - ---- - -## Entities - -Entities have stable identity. Equality is by `id`, not attributes. Use -`mutate(updater)` for state changes; it re-freezes and re-validates. Use -`AggregateId` or extend `DomainID` for custom identity types. - -### Example (from `@rineex/auth-core` OAuthAuthorization) - -```typescript -import { Entity, EntityProps, DomainID } from '@rineex/ddd'; - -// Custom ID – extend DomainID for domain-specific identifiers -class OAuthAuthorizationId extends DomainID {} - -export interface OAuthAuthorizationProps { - provider: string; - redirectUri: string; - scope: readonly string[]; -} - -export class OAuthAuthorization extends Entity< - OAuthAuthorizationId, - OAuthAuthorizationProps -> { - private constructor( - props: EntityProps, - ) { - super({ ...props }); - this.validate(); - } - - static create( - props: EntityProps, - ): OAuthAuthorization { - return new OAuthAuthorization(props); - } - - toObject(): Record { - return { - id: this.id.value, - provider: this.props.provider, - redirectUri: this.props.redirectUri, - scope: this.props.scope, - }; - } - - protected validateProps(props: OAuthAuthorizationProps): void { - if (!props.redirectUri.startsWith('https://')) { - throw new Error('Redirect URI must use HTTPS'); - } - } -} - -// Usage -const auth = OAuthAuthorization.create({ - id: OAuthAuthorizationId.generate(), - props: { - provider: 'google', - redirectUri: 'https://app.example.com/callback', - scope: ['openid', 'email'], - }, -}); -auth.equals(other); // true iff same id -``` - ---- - -## Aggregate Roots - -Aggregate roots extend `Entity` and add domain event support. - -### Example (from `aggregate-root.spec.ts`) - -```typescript -import { - AggregateRoot, - DomainEvent, - AggregateId, - EntityValidationError, -} from '@rineex/ddd'; - -interface OrderProps { - customerId: string; - total: number; -} - -class OrderCreatedEvent extends DomainEvent< - AggregateId, - { customerId: string } -> { - static create(props: { - id: string; - eventName: string; - aggregateId: AggregateId; - schemaVersion: number; - occurredAt: number; - payload: { customerId: string }; - }) { - return new OrderCreatedEvent(props); - } -} - -class OrderCompletedEvent extends DomainEvent { - static create(props: { - id: string; - eventName: string; - aggregateId: AggregateId; - schemaVersion: number; - occurredAt: number; - payload: { total: number }; - }) { - return new OrderCompletedEvent(props); - } -} - -class Order extends AggregateRoot { - constructor(params: { - id: AggregateId; - createdAt?: Date; - props: OrderProps; - }) { - super(params); - this.validate(); - } - - create(): void { - this.recordEvent( - OrderCreatedEvent.create({ - id: crypto.randomUUID(), - eventName: 'OrderCreated', - aggregateId: this.id, - schemaVersion: 1, - occurredAt: Date.now(), - payload: { customerId: this.props.customerId }, - }), - ); - } - - complete(): void { - this.recordEvent( - OrderCompletedEvent.create({ - id: crypto.randomUUID(), - eventName: 'OrderCompleted', - aggregateId: this.id, - schemaVersion: 1, - occurredAt: Date.now(), - payload: { total: this.props.total }, - }), - ); - } - - protected validateProps(props: OrderProps): void { - if (!props.customerId?.trim()) { - throw EntityValidationError.create('Customer ID is required', {}); - } - if (props.total < 0) { - throw EntityValidationError.create('Total must be non-negative', {}); - } - } - - toObject() { - return { - id: this.id.toString(), - createdAt: this.createdAt.toISOString(), - customerId: this.props.customerId, - total: this.props.total, - }; - } -} - -// Usage -const order = new Order({ - id: AggregateId.generate(), - props: { customerId: 'customer-1', total: 100 }, -}); -order.create(); -order.complete(); - -order.domainEvents; // readonly copy -const events = order.pullDomainEvents(); // returns and clears -``` - ---- - -## Domain Events - -Events are immutable. Payload must be JSON-safe (primitives, arrays, and plain -objects). Event factories must provide an ID and stable event name to the base -constructor; the base class never generates IDs through a runtime-specific API. - -### Example (from `domain.event.spec.ts`) - -```typescript -import { DomainEvent, DomainEventPayload, AggregateId } from '@rineex/ddd'; - -interface TestPayload extends DomainEventPayload { - userId: string; - action: string; -} - -class TestDomainEvent extends DomainEvent { - static create(props: { - id: string; - eventName: string; - aggregateId: AggregateId; - schemaVersion: number; - occurredAt: number; - payload: TestPayload; - }) { - return new TestDomainEvent(props); - } -} - -// Usage -const event = TestDomainEvent.create({ - id: crypto.randomUUID(), - eventName: 'TestEvent', - aggregateId: AggregateId.generate(), - schemaVersion: 1, - occurredAt: Date.now(), - payload: { userId: 'user-1', action: 'login' }, -}); - -event.id; -event.eventName; -event.aggregateId; -event.schemaVersion; -event.occurredAt; -event.payload; - -event.toPrimitives(); -// { id, eventName, aggregateId, schemaVersion, occurredAt, payload } -``` - ---- - -## Domain Errors - -### Base DomainError - -Extend `DomainError` with an explicit code literal and optional -primitive metadata. Register codes in a bounded-context registry const. - -```typescript -import { DomainError, InferErrorCodes, Metadata } from '@rineex/ddd'; - -export const UserErrorRegistry = { - USER: ['NOT_FOUND', 'INVALID_EMAIL'], -} as const; - -export type UserDomainErrorCode = InferErrorCodes; - -type Props = Metadata<{ identityId: string }>; - -class IdentityDisabledError extends DomainError<'USER.NOT_FOUND', Props> { - readonly code = 'USER.NOT_FOUND' as const; - - constructor(message: string, props: Props) { - super(message, props); - } -} -``` - -### Core error registry - -`@rineex/ddd` ships `CoreDomainErrorRegistry` and `InferErrorCodes` for built-in -codes (`DOMAIN.*`, `CORE.*`, `SYSTEM.*`). Each bounded context defines its own -registry: - -```typescript -import type { InferErrorCodes } from '@rineex/ddd'; - -export const MyModuleErrorRegistry = { - MY_MODULE: ['NOT_FOUND', 'INVALID_INPUT'], -} as const; - -export type MyModuleDomainErrorCode = InferErrorCodes< - typeof MyModuleErrorRegistry ->; -``` - -Use `registryErrorCodes(registry)` in architecture tests to verify every error -class code is registered. - -### Built-in Errors - -| Error | Code | Use case | -| ------------------------- | ------------------------ | ------------------------------------ | -| `InvalidValueObjectError` | `DOMAIN.INVALID_VALUE` | Value object validation failure | -| `EntityValidationError` | `CORE.VALIDATION_FAILED` | Entity/aggregate invariant violation | -| `InvalidValueError` | `DOMAIN.INVALID_VALUE` | Value constraint violation | -| `InvalidStateError` | `DOMAIN.INVALID_STATE` | Invalid state for operation | -| `InternalError` | `CORE.INTERNAL_ERROR` | Unexpected/programming errors | -| `TimeoutError` | `SYSTEM.TIMEOUT` | Operation timeout | -| `ApplicationError` | (extends `Error`) | Application/HTTP layer errors | - -```typescript -// InvalidValueError – optional metadata -throw new InvalidValueError('Age cannot be negative'); -throw new InvalidValueError('Validation failed', { - field: 'age', - min: 18, - max: 100, -}); - -// InvalidStateError – no metadata -throw new InvalidStateError('Cannot cancel completed order'); - -// EntityValidationError – props required -throw EntityValidationError.create('Name is required', {}); - -// ApplicationError – structured params -class UserNotFoundError extends ApplicationError { - constructor(userId: string) { - super({ - message: `User ${userId} not found`, - code: 'USER_NOT_FOUND', - isOperational: true, - metadata: { userId }, - }); - } -} -``` - ---- - -## Result Type - -`Result` models **application use-case outcomes** — -return expected failures instead of throwing. Domain entities and value objects -**throw** `DomainError` on invariant violations; application services return -`Result`. - -`E` is a per-use-case error union (`ApplicationError`, `DomainError`, etc.) — -there is no `DomainError` default. - -### Layer contract - -| Layer | Mechanism | -| -------------------------------- | ------------------------------------------ | -| Domain (entity, VO, aggregate) | Throw on invariant violation | -| Application (use case) | Return `Result` from service methods | -| Infrastructure (HTTP, messaging) | Unwrap `Result` at the boundary | - -`ApplicationServicePort` returns `Promise` — it is a structural seam for -orchestration. Application services that model expected failures should return -`Result` from their own `execute` methods (or wrap the port call at the -composition root). - -### Example - -```typescript -import { Result, UseCaseError, InvalidValueError } from '@rineex/ddd'; - -// Creation -const ok = Result.ok(42); -const voidOk = Result.ok(); // Ok for command use cases with no return value -const failed = Result.err(new InvalidValueError('Invalid')); - -// Narrowing -if (Result.isOk(ok)) { - const value = ok.value; // number -} -if (Result.isErr(failed)) { - const error = failed.error; // InvalidValueError -} - -// Untyped boundaries -const payload: unknown = JSON.parse('{"kind":"ok","value":42}'); -if (Result.isResult(payload)) { - // payload is Result -} - -// match -const message = Result.match(failed, { - ok: v => `ok ${v}`, - err: e => e.message, -}); -``` - -### Validation + chaining - -```typescript -function validateEmail(email: string): Result { - if (!email.includes('@')) { - return Result.err(new InvalidValueError('Invalid email format')); - } - return Result.ok(email); -} - -function createAccount( - email: string, -): Result<{ email: string }, InvalidValueError> { - return Result.flatMap(validateEmail(email), validated => - Result.ok({ email: validated }), - ); -} -``` - -### v4 → v5 migration - -| v4 | v5 | -| ---------------------------------- | ---------------------------------------------- | -| `Result.fail(e)` | `Result.err(e)` | -| `result.isSuccess` / `isFailure` | `Result.isOk(result)` / `Result.isErr(result)` | -| `result.getValue()` / `getError()` | `result.value` / `result.error` (after narrow) | - ---- - -## Application Services - -Use `ApplicationServicePort` for use-case orchestration. The port -signature is `execute(args: I): Promise` — it does not return `Result`. -Services that need explicit failure channels return `Result` from a dedicated -method or wrap domain outcomes at the caller. - -```typescript -import { ApplicationServicePort, Result, InvalidValueError } from '@rineex/ddd'; - -interface CreateUserInput { - name: string; - email: string; -} - -interface CreateUserOutput { - id: string; - name: string; -} - -class CreateUserService implements ApplicationServicePort< - CreateUserInput, - CreateUserOutput -> { - async execute(args: CreateUserInput): Promise { - // validate, create entity, persist, publish events - return { id: '...', name: args.name }; - } -} - -// Command with no return value — use Result.ok() -async function deactivateUser( - id: string, -): Promise> { - if (!id) return Result.err(new InvalidValueError('ID required')); - // ... persist - return Result.ok(); -} -``` - ---- - -## Ports & Utilities - -### ClockPort - -```typescript -import type { ClockPort } from '@rineex/ddd'; - -const clock: ClockPort = { - now: () => new Date(), -}; -``` - -### HttpStatus & HttpStatusMessage - -```typescript -import { HttpStatus, HttpStatusMessage } from '@rineex/ddd'; - -HttpStatus.OK; // 200 -HttpStatus.NOT_FOUND; // 404 -HttpStatusMessage[404]; // 'Not Found' -``` - -### deepFreeze - -```typescript -import { deepFreeze } from '@rineex/ddd'; - -const frozen = deepFreeze({ a: 1, nested: { b: 2 } }); -``` - ---- - -## Integration Guide - -1. **Add dependency:** `pnpm add @rineex/ddd` - -2. **Define an error registry** per bounded context (see - [Domain Errors](#domain-errors)). - -3. **Custom IDs:** Extend `DomainID` and use `generate()` / `fromString()`. - -4. **Use `mutate()`** for entity/aggregate state changes. - -5. **Persist then publish:** Save aggregate, then call `pullDomainEvents()` and - publish. - ---- - -## Core Concepts - -Value Objects, Entities, Aggregate Roots, and Domain Events are documented in -the sections above. Domain errors use registry-backed codes; application -outcomes use `Result`. - ---- - -## Examples - -See [Aggregate Roots](#aggregate-roots) (Order example) and -[Integration Guide](#integration-guide) for end-to-end patterns. - ---- - -## Best Practices - -- Extend `DomainID` for branded aggregate identifiers -- Use `mutate()` for entity state changes — never mutate `props` directly -- Throw `DomainError` in domain layer; return `Result` in application layer -- Register error codes in a bounded-context registry and verify with - architecture tests -- Call `pullDomainEvents()` after persistence, then publish - ---- - -## Error Handling - -See [Domain Errors](#domain-errors) for `DomainError`, registries, and built-in -error classes. - ---- - -## Contributing - -Develop in `packages/ddd`. Run `pnpm test`, `pnpm lint`, and `pnpm check-types` -from the package directory. Add a changeset for publishable changes: -`pnpm changeset` from the monorepo root. - ---- - -## API Reference - -### ValueObject\ - -| Member | Description | -| -------------------- | ------------------------ | -| `value` | Read-only props | -| `equals(other)` | Deep equality | -| `toJSON()` | Returns props | -| `toString()` | `JSON.stringify(props)` | -| `ValueObject.is(vo)` | Type guard | -| `validate(props)` | Abstract, must implement | - -### PrimitiveValueObject\ - -| Member | Description | -| ----------------- | --------------------- | -| `value` | Primitive value | -| `getValue()` | Same (deprecated) | -| `equals(other)` | Reference equality | -| `toString()` | String representation | -| `validate(value)` | Abstract | - -### Entity\ - -`Props` must be an object describing the entity's state. Do not use a scalar, -array, or DTO as entity props. - -| Member | Description | -| ---------------------- | --------------------------------------------- | -| `id` | Identity | -| `createdAt` | Creation date | -| `props` | Read-only (protected) | -| `equals(other)` | By `id` | -| `mutate(updater)` | Safe state change + revalidate | -| `validateProps(props)` | Protected abstract candidate-state validation | -| `toObject()` | Abstract | - -### AggregateRoot\ - -Extends `Entity`. Adds: - -| Member | Description | -| -------------------- | --------------------------------------------- | -| `recordEvent(event)` | Protected; append event after ownership check | -| `domainEvents` | Read-only copy | -| `pullDomainEvents()` | Return and clear | - -### DomainEvent\ - -| Member | Description | -| ---------------- | ------------------- | -| `id` | Event ID | -| `aggregateId` | Aggregate reference | -| `schemaVersion` | Version | -| `occurredAt` | Unix ms | -| `payload` | Serializable data | -| `eventName` | Stable event name | -| `toPrimitives()` | Plain object | - -### Result\ - -| Member | Description | -| ---------------------------------------------- | ----------------------------------------- | -| `Result.ok()` | Success with no value (`Ok`) | -| `Result.ok(value)` | Success (`{ kind: 'ok', value }`) | -| `Result.err(error)` | Failure (`{ kind: 'err', error }`) | -| `Result.isResult(value)` | Validates an unknown Result-shaped value | -| `Result.isOk(r)` / `Result.isErr(r)` | Type guards | -| `Result.match(r, { ok, err })` | Exhaustive fold | -| `Result.flatMap(r, fn)` | Chain use cases; forwards err | -| `Result.map(r, fn)` / `Result.mapError(r, fn)` | Transform value or error | -| `UseCaseError` | `{ code: string }` seam for error channel | - ---- - -## License +Start with `ValueObject` or `PrimitiveValueObject` for validated values, +`Entity` / `AggregateRoot` for identity-bearing state, and `Result` for explicit +success or error outcomes. Built-in value objects include `Email`, `Url`, +`Timestamp`, `DomainID`, and `AggregateId`. -Apache-2.0 – see [LICENSE](../../LICENSE). +See the [DDD primitives guide](../../docs/ddd.md) for the export map. diff --git a/packages/ddd/docs/DDD-GUIDE.md b/packages/ddd/docs/DDD-GUIDE.md deleted file mode 100644 index 992cf79..0000000 --- a/packages/ddd/docs/DDD-GUIDE.md +++ /dev/null @@ -1,102 +0,0 @@ -# DDD Guide for `@rineex/ddd` - -This package supplies tactical DDD building blocks. It does not decide your -bounded contexts, aggregate boundaries, or business language. Those decisions -belong to the application domain. - -## Where code belongs - -| Concern | Location | -| ----------------------------------------------------------------------- | ----------------- | -| Business rules, entities, value objects, aggregate roots, domain events | `domain/` | -| Use-case orchestration and transaction ports | `application/` | -| Database, ORM, HTTP, broker, and mapper implementations | `infrastructure/` | - -Dependencies point inward: infrastructure depends on application and domain; -domain depends on neither. - -## Entity - -Use `Entity` when an object has identity and behavior but is not the aggregate's -transaction boundary. Keep state protected and expose methods that express -business intent. - -```ts -class Account extends Entity { - private constructor(params: EntityProps) { - super(params); - this.validate(); - } - - static create(props: AccountProps): Account { - return new Account({ id: AccountId.generate(), props }); - } - - rename(name: string): void { - this.mutate(current => ({ ...current, name })); - } -} -``` - -Do not use an entity as a DTO, persistence row, or place for infrastructure -calls. - -## Aggregate root - -Use `AggregateRoot` when all changes to a consistency boundary must pass through -one object. Application services load and save the root; they do not update -child entities directly. - -An aggregate command should validate its business preconditions, mutate state -atomically, and then record a past-tense event. Cross-aggregate coordination -belongs in the application layer and is normally eventually consistent. - -## Value objects - -Use value objects for concepts defined by their values: email addresses, IDs, -URLs, money, status, and similar concepts. Prefer them over primitive strings or -numbers when validation or domain meaning matters. - -## Domain events - -Events describe facts that already happened. Use stable past-tense names such as -`AccountOpened`. Include a versioned JSON-safe payload. The aggregate records -the event; an application handler or outbox adapter publishes it. - -Do not put database writes, HTTP calls, event-bus calls, or command behavior in -an event class. - -## Mappers and rehydration - -Mappers are infrastructure adapters. They convert persistence primitives into -value objects and call the aggregate's explicit rehydration path. Rehydration -must restore state without emitting creation events. - -```ts -toDomain(row: AccountRow): Account { - return Account.rehydrate({ - id: AccountId.fromString(row.id), - props: { name: row.name, status: AccountStatus.fromString(row.status) }, - createdAt: row.createdAt, - }); -} -``` - -## Testing checklist - -- valid creation and invalid construction; -- every public command's successful transition; -- failed transitions leave state unchanged; -- aggregate event type, ID, payload, and order; -- rehydration emits no creation event; -- serialization contains primitives and no infrastructure objects. - -## Application outcomes - -Use `Result` in application services for expected failures. At untyped -boundaries such as HTTP or messaging, call `Result.isResult(value)` before using -a value as a result. The guard validates the `kind` discriminant and its -required branch field; it does not validate the contained payload or error. - -Do not use `Result` for domain invariant violations: entities and value objects -throw domain errors when their invariants are violated. diff --git a/packages/ddd/docs/MIGRATION.md b/packages/ddd/docs/MIGRATION.md deleted file mode 100644 index 8943e58..0000000 --- a/packages/ddd/docs/MIGRATION.md +++ /dev/null @@ -1,48 +0,0 @@ -# DDD API Migration - -The DDD primitives are being hardened with an intentional breaking change. - -## Planned changes - -- Base constructors no longer call overridable validation methods. -- Concrete entities and aggregates validate explicitly in creation and - rehydration factories. -- `mutate` validates candidate state before committing it. -- Aggregate event recording becomes protected and checks aggregate identity. -- Domain events require explicit IDs and timestamps and no longer generate IDs - through `node:crypto`. -- Event metadata and payload serialization become stricter and immutable. - -## Migration shape - -Before: - -```ts -constructor(params: EntityProps) { - super(params); -} -``` - -After: - -```ts -private constructor(params: EntityProps) { - super(params); - this.validate(); -} - -static rehydrate(params: EntityProps): Account { - return new Account(params); -} -``` - -Creation factories may record creation events after construction. Rehydration -factories must not. - -## Entity property types - -`Entity` and `AggregateRoot` property generics must be object shapes. If a -property type was previously rejected because it did not satisfy `EmptyObject`, -no migration is needed: ordinary interfaces and object types are supported. -Primitive value objects still support booleans, but boolean value objects must -not be used as entity IDs. diff --git a/packages/decision-engine/README.md b/packages/decision-engine/README.md index e011d40..35e73e4 100644 --- a/packages/decision-engine/README.md +++ b/packages/decision-engine/README.md @@ -1,156 +1,16 @@ # @rineex/decision-engine -A framework-independent, synchronous engine for making explainable selections -from a set of candidates. It evaluates hard constraints, normalizes feature -values, scores and ranks eligible candidates, selects a final subset, and -returns the complete decision trail plus domain events. +A deterministic, synchronous decision pipeline that evaluates constraints, +normalizes features, scores, ranks, selects, and returns frozen results and +domain events. It does no I/O or event publication. -## Installation - -```sh +```bash pnpm add @rineex/decision-engine ``` -## Quick start - -```ts -import { - DecisionEngine, - DefaultDecisionValidator, - DescendingScoreRankingStrategy, - FeatureObjective, - MinMaxNormalizer, - SelectTopNStrategy, - WeightedSumScoringStrategy, -} from '@rineex/decision-engine'; - -interface Supplier { - readonly id: string; - readonly approved: boolean; - readonly quality: number; - readonly cost: number; -} - -const suppliers: readonly Supplier[] = [ - { id: 'acme', approved: true, quality: 98, cost: 125 }, - { id: 'beta', approved: true, quality: 80, cost: 80 }, - { id: 'blocked', approved: false, quality: 95, cost: 70 }, -]; - -const engine = new DecisionEngine(new DefaultDecisionValidator()); - -const execution = engine.execute( - { - id: 'supplier-selection', - version: 'v1', - candidateRefResolver: { resolve: candidate => candidate.id }, - constraints: [ - { - id: 'approved', - evaluate: supplier => - supplier.approved - ? { constraintId: 'approved', satisfied: true } - : { - constraintId: 'approved', - satisfied: false, - reasonCode: 'NOT_APPROVED', - }, - }, - ], - features: [ - { - key: 'quality', - objective: FeatureObjective.MAXIMIZE, - evaluate: supplier => ({ - key: 'quality', - objective: FeatureObjective.MAXIMIZE, - rawValue: supplier.quality, - }), - }, - { - key: 'cost', - objective: FeatureObjective.MINIMIZE, - evaluate: supplier => ({ - key: 'cost', - objective: FeatureObjective.MINIMIZE, - rawValue: supplier.cost, - }), - }, - ], - normalizer: new MinMaxNormalizer(), - scoringStrategy: new WeightedSumScoringStrategy({ - resolve: key => (key === 'quality' ? 0.7 : 0.3), - }), - rankingStrategy: new DescendingScoreRankingStrategy(), - selectionStrategy: new SelectTopNStrategy(1), - }, - { - candidates: suppliers, - context: undefined, - policy: { id: 'default', version: 'v1', values: {} }, - }, -); - -console.log( - execution.result.selected.map(evaluation => evaluation.candidate.id), -); -// ['acme'] -``` - -## Execution model - -`execute` is synchronous and has no I/O. It performs these stages in order: - -1. Validate the definition and request. -2. Resolve and validate unique candidate references. -3. Evaluate hard constraints for every candidate. -4. Extract, normalize, score, rank, and select eligible candidates. -5. Return immutable result data and events. - -Rejected candidates are retained in `result.evaluations` with their constraint -results, but have no features, score, rank, or selection flag. This makes the -outcome explainable without re-running the decision. - -## Built-in strategies - -| Component | Purpose | -| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `MinMaxNormalizer` | Maps each feature across eligible candidates to `0..1`; minimize objectives are inverted and zero-range features become `1`. | -| `WeightedSumScoringStrategy` | Multiplies each normalized feature by a policy-derived finite weight. | -| `DescendingScoreRankingStrategy` | Ranks higher scores first while preserving input order for ties. | -| `SelectFirstStrategy` | Selects the highest-ranked candidate. | -| `SelectTopNStrategy(n)` | Selects up to `n` ranked candidates. | -| `SelectAboveThresholdStrategy(score)` | Selects ranked candidates at or above the score threshold. | - -## Results and events - -`execution.result` contains the selected evaluations and every candidate -evaluation. `execution.events` includes one `candidate.rejected` event for each -rejected candidate and one `decision.completed` event. Output collections and -engine-owned records are frozen; supplied candidate objects are not modified. - -The engine creates events but never delivers them. Publish them explicitly at -your application boundary: - -```ts -import { - type DecisionCompletedEvent, - InMemoryDecisionEventPublisher, -} from '@rineex/decision-engine'; - -const publisher = new InMemoryDecisionEventPublisher(); -publisher.subscribe('decision.completed', { - handle: async event => { - console.log(event.payload.selectedCandidateRefs); - }, -}); - -await publisher.publish(execution.events); -``` - -## Errors +Built-ins include `MinMaxNormalizer`, `WeightedSumScoringStrategy`, +`DescendingScoreRankingStrategy`, and first/top-N/threshold selection +strategies. Use `InMemoryDecisionEventPublisher` outside the engine when an +in-memory dispatcher is appropriate. -Invalid definitions throw `InvalidDecisionDefinitionError`. Invalid runtime -input or failed component execution throws `DecisionExecutionError`. Both extend -`DecisionError` and expose a stable `code`, structured `details`, and the -original `cause` when available. +Read the [complete decision-engine example](../../docs/decision-engine.md). diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md new file mode 100644 index 0000000..08f6219 --- /dev/null +++ b/packages/eslint-config/README.md @@ -0,0 +1,14 @@ +# @rineex/eslint-config + +Private shared ESLint flat configurations for the Rineex Core workspace. + +The package exposes four configuration entry points: + +- `@rineex/eslint-config/base` +- `@rineex/eslint-config/next-js` +- `@rineex/eslint-config/react-internal` +- `@rineex/eslint-config/db` + +Use an entry point from a package-level `eslint.config.mjs` and spread its +exported configuration into the local flat-config array. This package is +workspace tooling, not a runtime dependency for applications. diff --git a/packages/ioredis/README.md b/packages/ioredis/README.md index cc6115a..feba882 100644 --- a/packages/ioredis/README.md +++ b/packages/ioredis/README.md @@ -1,225 +1,15 @@ # @rineex/ioredis -## Overview - -`@rineex/ioredis` provides a NestJS integration for -[`ioredis`](https://www.npmjs.com/package/ioredis), including: - -- Creating and exporting Redis clients (single instance or cluster) via a - configurable module (`RedisModule`) -- Injecting clients by name via `@InjectRedis()` -- Basic lifecycle management: - - On application bootstrap: ping all tracked clients and log results - - On application shutdown: `quit()` all tracked clients -- A Terminus health indicator and module (`RedisHealthIndicator`, - `RedisHealthModule`) - -Use this when: - -- You want an `ioredis` client managed by NestJS DI, with support for multiple - named connections. -- You want the module to close Redis connections on shutdown. - -Do not use this when: - -- You do not use NestJS DI (you can use `ioredis` directly). -- You need a different Redis library than `ioredis`. - -## Public API - -### Exports - -The package exports: - -- `RedisModule` -- `InjectRedis` -- `RedisHealthIndicator`, `RedisHealthModule` -- `RedisModuleOptions`, `RedisModuleExtraOptions` -- `getRedisConnectionToken`, `getRedisOptionsToken` - -### `RedisModule` - -A NestJS configurable module for provisioning an `ioredis` client and exporting -it under a DI token. - -- **register(options: RedisModuleOptions, extras?: RedisModuleExtraOptions)** - - **options**: - - `type: 'single' | 'cluster'` - - If `type: 'single'`: - - `url?: string` - - `options?: import('ioredis').RedisOptions` - - If `type: 'cluster'`: - - `nodes: import('ioredis').ClusterNode[]` - - `options?: import('ioredis').ClusterOptions` - - **extras**: - - `connection?: string` (default: `'default'`) - - **returns**: a dynamic Nest module that exports a provider for the client. - -- **registerAsync(...)** - - Async variant; the factory must resolve to `RedisModuleOptions`. - - Supports the same `extras.connection` naming behavior. - -### Injection helpers - -- **InjectRedis(connection?: string)** - - Parameter decorator that injects the `ioredis` client for the given - connection name. - - If `connection` is omitted, injects the default connection (`'default'`). - -- **getRedisConnectionToken(connection?: string): string** - - Returns the DI token used to register/inject the client. - -- **getRedisOptionsToken(connection?: string): string** - - Returns the DI token used internally for module options for a given - connection. - -### Health check integration - -- **RedisHealthModule** - - Nest module exporting `RedisHealthIndicator`. - - Provides a `REDIS_HEALTH_INDICATOR` client via a factory that calls - `new Redis()` with default settings. - -- **RedisHealthIndicator** - - `isHealthy(key: string): Promise` - - **Behavior**: - - `PING` succeeds → returns healthy status for `key` - - `PING` fails → throws `HealthCheckError` with a status payload including - `message` - -## Usage Examples - -### Single Redis (default connection) - -```ts -import { Module, Injectable } from '@nestjs/common'; -import type Redis from 'ioredis'; -import { InjectRedis, RedisModule } from '@rineex/ioredis'; - -@Module({ - imports: [ - RedisModule.register({ - type: 'single', - url: process.env.REDIS_URL ?? 'redis://localhost:6379', - }), - ], -}) -export class AppModule {} - -@Injectable() -export class CacheService { - constructor(@InjectRedis() private readonly redis: Redis) {} - - async setValue(key: string, value: string) { - await this.redis.set(key, value); - } -} -``` - -### Named connection - -```ts -@Module({ - imports: [ - RedisModule.register( - { type: 'single', url: 'redis://localhost:6380' }, - { connection: 'cache' }, - ), - ], -}) -export class AppModule {} -``` - -```ts -@Injectable() -export class CacheService { - constructor(@InjectRedis('cache') private readonly redis: Redis) {} -} -``` - -### Cluster - -```ts -import type { Cluster } from 'ioredis'; -import { Module, Injectable } from '@nestjs/common'; -import { InjectRedis, RedisModule } from '@rineex/ioredis'; - -@Module({ - imports: [ - RedisModule.register( - { - type: 'cluster', - nodes: [{ host: '127.0.0.1', port: 7000 }], - }, - { connection: 'cluster' }, - ), - ], -}) -export class AppModule {} - -@Injectable() -export class ClusterService { - constructor(@InjectRedis('cluster') private readonly cluster: Cluster) {} -} -``` - -### Terminus health check - -```ts -import { Controller, Get } from '@nestjs/common'; -import { HealthCheck, HealthCheckService } from '@nestjs/terminus'; -import { RedisHealthIndicator } from '@rineex/ioredis'; - -@Controller('health') -export class HealthController { - constructor( - private readonly health: HealthCheckService, - private readonly redis: RedisHealthIndicator, - ) {} - - @Get() - @HealthCheck() - check() { - return this.health.check([() => this.redis.isHealthy('redis')]); - } -} -``` - -## Behavior & Guarantees - -- **Connection naming**: if `extras.connection` is omitted, the connection name - is `'default'`. -- **Lifecycle**: - - Clients created by `RedisModule` are tracked via `WeakRef` and pinged during - `onApplicationBootstrap()`. - - On shutdown, the module attempts to `quit()` each tracked client; failures - are logged and do not throw. -- **Concurrency**: clients are shared singletons per DI token; concurrent use - follows `ioredis` semantics. - -## Operational Notes - -- **Configuration** - - For single instances, prefer a `url` (e.g. `redis://host:6379`) and pass - `options` for advanced settings. - - For cluster, supply `nodes` and optional `ClusterOptions`. -- **Logging** - - Bootstrap health checks log ping success/failure per client. - - Shutdown logs success or error when closing connections. -- **Health module default client** - - `RedisHealthModule` creates a fresh `new Redis()` with default - configuration; override the `REDIS_HEALTH_INDICATOR` provider if you need to - health-check a configured/named connection instead of the default. - -## Development +NestJS support for named ioredis single-node or cluster connections, plus a +Terminus-compatible health indicator. ```bash -pnpm build -pnpm test -pnpm lint -pnpm check-types +pnpm add @rineex/ioredis ioredis @nestjs/common @nestjs/terminus ``` -## License +Register `RedisModule` with either a single URL/options object or cluster nodes, +then inject a tracked client with `@InjectRedis(connectionName)`. The module +pings tracked clients on bootstrap and closes them on shutdown. Import +`RedisHealthModule` to use `RedisHealthIndicator.isHealthy(key)`. -Apache-2.0 +See [NestJS integrations](../../docs/nestjs-integrations.md) for usage. diff --git a/packages/libs/README.md b/packages/libs/README.md index c81d529..521e49b 100644 --- a/packages/libs/README.md +++ b/packages/libs/README.md @@ -1,26 +1,7 @@ # @rineex/libs -Shared utility libraries for Rineex core modules. +Workspace package reserved for shared libraries. -> **Note:** This package is published but most utilities are not yet exported -> from the public entry. `src/index.ts` currently exports an empty object. -> Internal utilities (e.g. `default-if-blank`) exist in source for monorepo use. - -## Installation - -```bash -pnpm add @rineex/libs -``` - -## Development - -```bash -pnpm build -pnpm test -pnpm lint -pnpm check-types -``` - -## License - -Apache-2.0 +Its current `src/index.ts` exports nothing, so it has no supported consumer API. +The `strings/default-if-blank.ts` source file is not re-exported and must not be +consumed through a deep import. diff --git a/packages/nest/cookie-middleware-module/README.md b/packages/nest/cookie-middleware-module/README.md index e7978f0..8142a9e 100644 --- a/packages/nest/cookie-middleware-module/README.md +++ b/packages/nest/cookie-middleware-module/README.md @@ -1,215 +1,14 @@ -# Cookie Parser Middleware Module +# @rineex/cookie-parser-mw-module -## Overview +NestJS module that applies `cookie-parser` middleware to every route. -`CookieParserModule` is a NestJS module that integrates the `cookie-parser` -middleware into NestJS applications. It parses HTTP request cookies and makes -them available via `req.cookies` and `req.signedCookies`. - -**When to use:** - -- Applications that need to read HTTP cookies from incoming requests -- Applications using cookie-based authentication or session management -- Applications that need signed cookie support - -**When not to use:** - -- Applications that only set cookies but never read them -- Applications using alternative cookie parsing libraries -- Applications that handle cookies at a different layer (e.g., API gateway) - -## Public API - -### `CookieParserModule` - -A NestJS module that configures and applies cookie parsing middleware globally. - -#### Static Methods - -##### `CookieParserModule.forRoot(options?)` - -Synchronously configures the module with cookie parser options. - -**Parameters:** - -- `options` (optional): `CookieParseOptions` - Configuration options for - cookie-parser - - `secret` (optional): `string | string[]` - Secret(s) for signing cookies. If - provided, enables signed cookie support via `req.signedCookies` - - All other options from `cookie-parser` are supported - -**Returns:** `DynamicModule` - A configured NestJS module - -**Example:** - -```ts -CookieParserModule.forRoot({ - secret: 'my-secret-key', -}); -``` - -##### `CookieParserModule.forRootAsync(options)` - -Asynchronously configures the module, useful when options depend on other -providers or configuration. - -**Parameters:** - -- `options`: Object with one of: - - `useFactory`: - `(...args: any[]) => CookieParseOptions | Promise` - - Factory function - - `useClass`: Constructor class implementing - `ConfigurableModuleOptionsFactory` - - `useExisting`: Token for existing provider - - `imports` (optional): Array of modules to import - - `inject` (optional): Array of injection tokens for factory dependencies - -**Returns:** `DynamicModule` - A configured NestJS module - -**Example:** - -```ts -CookieParserModule.forRootAsync({ - useFactory: (config: ConfigService) => ({ - secret: config.get('COOKIE_SECRET'), - }), - inject: [ConfigService], -}); -``` - -### `CookieParserMiddleware` - -Internal middleware class that wraps `cookie-parser`. Not intended for direct -use. - -## Usage Examples - -### Basic Setup - -```ts -import { Module } from '@nestjs/common'; -import { CookieParserModule } from '@rineex/cookie-parser-mw-module'; - -@Module({ - imports: [CookieParserModule.forRoot()], -}) -export class AppModule {} -``` - -### With Secret for Signed Cookies - -```ts -import { Module } from '@nestjs/common'; -import { CookieParserModule } from '@rineex/cookie-parser-mw-module'; - -@Module({ - imports: [ - CookieParserModule.forRoot({ - secret: 'your-secret-key', - }), - ], -}) -export class AppModule {} -``` - -### Async Configuration - -```ts -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { CookieParserModule } from '@rineex/cookie-parser-mw-module'; - -@Module({ - imports: [ - ConfigModule.forRoot(), - CookieParserModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (config: ConfigService) => ({ - secret: config.get('COOKIE_SECRET'), - }), - inject: [ConfigService], - }), - ], -}) -export class AppModule {} -``` - -### Accessing Cookies in Controllers - -```ts -import { Controller, Get, Req } from '@nestjs/common'; -import { Request } from 'express'; - -@Controller() -export class AppController { - @Get() - getCookies(@Req() req: Request) { - // Regular cookies - const regularCookie = req.cookies?.sessionId; - - // Signed cookies (if secret is configured) - const signedCookie = req.signedCookies?.token; - - return { regularCookie, signedCookie }; - } -} +```bash +pnpm add @rineex/cookie-parser-mw-module cookie-parser @nestjs/common @nestjs/core express ``` -## Behavior & Guarantees - -### Invariants - -- Middleware is applied to all routes (`forRoutes('*')`) -- Middleware runs before route handlers -- `req.cookies` is always an object (empty if no cookies) -- `req.signedCookies` is available only if a secret is configured -- Invalid or tampered signed cookies are excluded from `req.signedCookies` but - logged as errors - -### Performance - -- Cookie parsing is synchronous and lightweight -- Parsing occurs once per request -- No caching or memoization (stateless per request) - -### Concurrency - -- Stateless middleware; safe for concurrent requests -- No shared state between requests - -## Operational Notes - -### Configuration - -The module accepts all options from `cookie-parser`'s `CookieParseOptions`: - -- `secret`: Enables signed cookie verification -- Multiple secrets: Pass an array to support secret rotation - -### Cookie Access - -After middleware execution: - -- `req.cookies`: Plain cookies as key-value pairs -- `req.signedCookies`: Verified signed cookies (only if secret configured) - -### Common Pitfalls - -1. **Missing secret for signed cookies**: If you set signed cookies but don't - configure a secret, they won't appear in `req.signedCookies` -2. **Secret mismatch**: Signed cookies created with one secret won't verify with - a different secret -3. **Order matters**: Ensure `CookieParserModule` is imported before modules - that depend on cookies -4. **Route-specific application**: The middleware applies to all routes. For - selective application, use the middleware directly instead of this module - -### Logging - -- Invalid signed cookies are logged as errors by `cookie-parser` -- No additional logging is provided by this module - -## License +Import `CookieParserModule.register(options)` with `CookieParseOptions`, or use +`registerAsync`. The package exports `CookieParserModule`, +`CookieParserMiddleware`, and the upstream `CookieParseOptions` type. -Apache-2.0 +See [NestJS integrations](../../../docs/nestjs-integrations.md) for the shared +middleware pattern. diff --git a/packages/nest/cors-middleware-module/README.md b/packages/nest/cors-middleware-module/README.md index fcfae6d..b381a92 100644 --- a/packages/nest/cors-middleware-module/README.md +++ b/packages/nest/cors-middleware-module/README.md @@ -1,170 +1,15 @@ -# CORS Middleware Module +# @rineex/cors-mw-module -## Overview +NestJS module that applies `cors` middleware to every route. -`CorsModule` integrates CORS (Cross-Origin Resource Sharing) middleware into -NestJS applications. It handles preflight OPTIONS requests and sets appropriate -CORS headers on responses. - -**When to use:** - -- APIs accessed from web browsers with different origins -- Applications requiring cross-origin requests with credentials -- REST APIs consumed by frontend applications - -**When not to use:** - -- Internal-only APIs not accessed from browsers -- APIs behind an API gateway that handles CORS -- Server-to-server communication only - -## Public API - -### `CorsModule` - -NestJS module that configures and applies CORS middleware globally. - -#### Static Methods - -##### `CorsModule.forRoot(options?)` - -Synchronously configures the module with CORS options. - -**Parameters:** - -- `options` (optional): `CorsOptions` - Configuration options from - `@nestjs/common` - - Merged with defaults: `credentials: true`, common HTTP methods, and standard - headers - - All standard CORS options supported (origin, methods, allowedHeaders, etc.) - -**Returns:** `DynamicModule` - A configured NestJS module - -##### `CorsModule.forRootAsync(options)` - -Asynchronously configures the module. - -**Parameters:** - -- `options`: Object with one of: - - `useFactory`: `(...args: any[]) => CorsOptions | Promise` - - `useClass`: Constructor class - - `useExisting`: Token for existing provider - - `imports` (optional): Array of modules - - `inject` (optional): Array of injection tokens - -**Returns:** `DynamicModule` - A configured NestJS module - -### Default Configuration - -- `credentials: true` - Allows cookies/credentials in cross-origin requests -- `methods`: GET, POST, PUT, PATCH, DELETE -- `allowedHeaders`: Includes common headers (Content-Type, Authorization, custom - x-\* headers, etc.) -- `preflightContinue: false` - Terminates preflight requests -- `optionsSuccessStatus: 204` - HTTP status for successful preflight - -## Usage Examples - -### Basic Setup - -```ts -import { Module } from '@nestjs/common'; -import { CorsModule } from '@rineex/cors-mw-module'; - -@Module({ - imports: [CorsModule.forRoot()], -}) -export class AppModule {} +```bash +pnpm add @rineex/cors-mw-module cors @nestjs/common @nestjs/core express ``` -### With Custom Origin - -```ts -import { Module } from '@nestjs/common'; -import { CorsModule } from '@rineex/cors-mw-module'; - -@Module({ - imports: [ - CorsModule.forRoot({ - origin: 'https://example.com', - }), - ], -}) -export class AppModule {} -``` - -### Async Configuration - -```ts -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { CorsModule } from '@rineex/cors-mw-module'; - -@Module({ - imports: [ - ConfigModule.forRoot(), - CorsModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (config: ConfigService) => ({ - origin: config.get('ALLOWED_ORIGINS')?.split(','), - }), - inject: [ConfigService], - }), - ], -}) -export class AppModule {} -``` - -## Behavior & Guarantees - -### Invariants - -- Middleware applies to all routes (`forRoutes('*')`) -- Preflight OPTIONS requests are handled automatically -- Default options are merged with user-provided options (user options take - precedence) -- CORS headers are set on all responses when conditions match - -### Performance - -- Minimal overhead per request -- Preflight requests are handled synchronously -- No caching of CORS decisions (evaluated per request) - -### Concurrency - -- Stateless middleware; safe for concurrent requests -- No shared state between requests - -## Operational Notes - -### Configuration - -User-provided options override defaults. Common options: - -- `origin`: String, array, or function returning allowed origins -- `methods`: Array of allowed HTTP methods -- `allowedHeaders`: Array of allowed request headers -- `exposedHeaders`: Array of headers exposed to client -- `credentials`: Boolean for allowing credentials -- `maxAge`: Number of seconds to cache preflight response - -### Common Pitfalls - -1. **Wildcard origin with credentials**: `origin: '*'` cannot be used with - `credentials: true` -2. **Missing methods**: Ensure required HTTP methods are included -3. **Header case sensitivity**: Headers are case-insensitive per HTTP spec, but - some clients are sensitive -4. **Preflight caching**: Set `maxAge` appropriately to reduce preflight - requests - -### Logging - -- No logging provided by this module -- CORS rejections are handled silently (browser console shows errors) - -## License +Import `CorsModule.register(options)` or `registerAsync`. Defaults enable +credentials, use the module's allowed methods and headers, and return `204` for +preflight responses. Passed `CorsOptions` override those defaults. The package +exports `CorsModule` and `CorsMiddleware`. -Apache-2.0 +See [NestJS integrations](../../../docs/nestjs-integrations.md) for the shared +middleware pattern. diff --git a/packages/nest/helmet-middleware-module/README.md b/packages/nest/helmet-middleware-module/README.md index b500f7a..72c0f1a 100644 --- a/packages/nest/helmet-middleware-module/README.md +++ b/packages/nest/helmet-middleware-module/README.md @@ -1,169 +1,13 @@ -# Helmet Middleware Module +# @rineex/helmet-mw-module -## Overview +NestJS module that applies `helmet` middleware to every route. -`HelmetModule` integrates Helmet security middleware into NestJS applications. -Helmet sets various HTTP security headers to help protect against common web -vulnerabilities. - -**When to use:** - -- Web applications exposed to browsers -- APIs that serve HTML content -- Applications requiring security header compliance (CSP, HSTS, etc.) - -**When not to use:** - -- Internal-only APIs not accessed from browsers -- APIs behind a reverse proxy that sets security headers -- Applications with custom security header requirements that conflict with - Helmet defaults - -## Public API - -### `HelmetModule` - -NestJS module that configures and applies Helmet middleware globally. - -#### Static Methods - -##### `HelmetModule.forRoot(options?)` - -Synchronously configures the module with Helmet options. - -**Parameters:** - -- `options` (optional): `HelmetOptions` - Configuration options from `helmet` - package - - Supports all Helmet sub-middleware options (contentSecurityPolicy, hsts, - frameguard, etc.) - - If omitted, uses Helmet's default secure configuration - -**Returns:** `DynamicModule` - A configured NestJS module - -##### `HelmetModule.forRootAsync(options)` - -Asynchronously configures the module. - -**Parameters:** - -- `options`: Object with one of: - - `useFactory`: `(...args: any[]) => HelmetOptions | Promise` - - `useClass`: Constructor class - - `useExisting`: Token for existing provider - - `imports` (optional): Array of modules - - `inject` (optional): Array of injection tokens - -**Returns:** `DynamicModule` - A configured NestJS module - -## Usage Examples - -### Basic Setup - -```ts -import { Module } from '@nestjs/common'; -import { HelmetModule } from '@rineex/helmet-mw-module'; - -@Module({ - imports: [HelmetModule.forRoot()], -}) -export class AppModule {} +```bash +pnpm add @rineex/helmet-mw-module helmet @nestjs/common @nestjs/core express ``` -### With Custom Options - -```ts -import { Module } from '@nestjs/common'; -import { HelmetModule } from '@rineex/helmet-mw-module'; - -@Module({ - imports: [ - HelmetModule.forRoot({ - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'"], - }, - }, - }), - ], -}) -export class AppModule {} -``` - -### Async Configuration - -```ts -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { HelmetModule } from '@rineex/helmet-mw-module'; - -@Module({ - imports: [ - ConfigModule.forRoot(), - HelmetModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (config: ConfigService) => ({ - hsts: { - maxAge: config.get('HSTS_MAX_AGE', 31536000), - }, - }), - inject: [ConfigService], - }), - ], -}) -export class AppModule {} -``` - -## Behavior & Guarantees - -### Invariants - -- Middleware applies to all routes (`forRoutes('*')`) -- Security headers are set on all HTTP responses -- Default configuration enables all Helmet sub-middlewares with secure defaults - -### Performance - -- Minimal overhead per request -- Header setting is synchronous -- No external dependencies or network calls - -### Concurrency - -- Stateless middleware; safe for concurrent requests -- No shared state between requests - -## Operational Notes - -### Configuration - -Helmet options include: - -- `contentSecurityPolicy`: Content Security Policy headers -- `hsts`: HTTP Strict Transport Security -- `frameguard`: X-Frame-Options header -- `noSniff`: X-Content-Type-Options header -- `xssFilter`: X-XSS-Protection header -- `referrerPolicy`: Referrer-Policy header -- And other security-related headers - -### Common Pitfalls - -1. **CSP breaking third-party scripts**: Overly strict Content Security Policy - can block legitimate resources -2. **HSTS in development**: Long HSTS maxAge can cause issues during local - development -3. **Iframe embedding**: Default frameguard blocks iframe embedding; disable if - needed for embeddable widgets -4. **Content-Type sniffing**: `noSniff` prevents MIME type sniffing, which may - break some legacy clients - -### Logging - -- No logging provided by this module -- Helmet errors are handled internally and don't affect request processing - -## License +Import `HelmetModule.register(options)` with `HelmetOptions`, or use +`registerAsync`. The package exports `HelmetModule` and `HelmetMiddleware`. -Apache-2.0 +See [NestJS integrations](../../../docs/nestjs-integrations.md) for the shared +middleware pattern. diff --git a/packages/nest/no-favicon-middleware-module/README.md b/packages/nest/no-favicon-middleware-module/README.md index 826230b..728f605 100644 --- a/packages/nest/no-favicon-middleware-module/README.md +++ b/packages/nest/no-favicon-middleware-module/README.md @@ -1,121 +1,14 @@ -# Favicon Ignore Middleware Module +# @rineex/favicon-ignore-mw-module -## Overview +NestJS module that returns HTTP `204` for requests whose original URL contains +`favicon.ico`. -`IgnoreFaviconModule` intercepts requests for `favicon.ico` and returns -`204 No Content`, preventing these requests from reaching route handlers or -generating 404 errors. - -**When to use:** - -- Applications without a favicon file -- APIs that want to suppress favicon request logs -- Applications where favicon requests should be handled silently - -**When not to use:** - -- Applications that serve a favicon file -- Applications using static file serving that handles favicon requests -- Applications behind a reverse proxy that handles favicon requests - -## Public API - -### `IgnoreFaviconModule` - -NestJS module that applies favicon-ignoring middleware globally. - -#### Static Methods - -##### `IgnoreFaviconModule.forRoot()` - -Configures the module. No options required. - -**Returns:** `DynamicModule` - A configured NestJS module - -##### `IgnoreFaviconModule.forRootAsync(options?)` - -Asynchronously configures the module. Options are optional and typically unused. - -**Parameters:** - -- `options` (optional): Standard async module options (useFactory, imports, - etc.) - -**Returns:** `DynamicModule` - A configured NestJS module - -## Usage Examples - -### Basic Setup - -```ts -import { Module } from '@nestjs/common'; -import { IgnoreFaviconModule } from '@rineex/favicon-ignore-mw-module'; - -@Module({ - imports: [IgnoreFaviconModule.forRoot()], -}) -export class AppModule {} +```bash +pnpm add @rineex/favicon-ignore-mw-module @nestjs/common @nestjs/core express ``` -### Async Setup - -```ts -import { Module } from '@nestjs/common'; -import { IgnoreFaviconModule } from '@rineex/favicon-ignore-mw-module'; - -@Module({ - imports: [IgnoreFaviconModule.forRootAsync()], -}) -export class AppModule {} -``` - -## Behavior & Guarantees - -### Invariants - -- Middleware applies to all routes (`forRoutes('*')`) -- Requests with `favicon.ico` in the URL path return `204 No Content` -- All other requests pass through unchanged -- No response body is sent for favicon requests - -### Performance - -- Minimal overhead: simple string check on request URL -- Early return prevents route handler execution for favicon requests - -### Concurrency - -- Stateless middleware; safe for concurrent requests -- No shared state between requests - -## Operational Notes - -### Configuration - -No configuration options. The module works out of the box. - -### Matching Logic - -The middleware checks if `req.originalUrl` includes the string `'favicon.ico'`. -This matches: - -- `/favicon.ico` -- `/path/to/favicon.ico` -- Any URL containing `favicon.ico` as a substring - -### Common Pitfalls - -1. **Path matching**: Matches any URL containing `favicon.ico`, not just - `/favicon.ico` -2. **Case sensitivity**: Matching is case-sensitive; `Favicon.ico` won't match -3. **Query parameters**: URLs like `/favicon.ico?v=1` will match - -### Logging - -- No logging provided by this module -- Favicon requests return `204` without triggering route handlers or error - handlers - -## License +Import `IgnoreFaviconModule.register()` or `registerAsync`. The package exports +`IgnoreFaviconModule` and `IgnoreFaviconMiddleware`. -Apache-2.0 +See [NestJS integrations](../../../docs/nestjs-integrations.md) for the shared +middleware pattern. diff --git a/packages/nest/response-time-middleware-module/README.md b/packages/nest/response-time-middleware-module/README.md index da1d2c5..ce8886a 100644 --- a/packages/nest/response-time-middleware-module/README.md +++ b/packages/nest/response-time-middleware-module/README.md @@ -1,170 +1,14 @@ -# Response Time Middleware Module +# @rineex/response-time-mw-module -## Overview +NestJS module that applies `response-time` middleware to every route. -`ResponseTimeModule` adds response time headers to HTTP responses, measuring the -time taken to process each request. This is useful for performance monitoring -and debugging. - -**When to use:** - -- Applications requiring response time metrics in headers -- APIs that need client-visible performance indicators -- Development environments for performance debugging - -**When not to use:** - -- Applications using APM tools that measure response times internally -- Applications behind reverse proxies that add response time headers -- Applications where header overhead is a concern - -## Public API - -### `ResponseTimeModule` - -NestJS module that configures and applies response time middleware globally. - -#### Static Methods - -##### `ResponseTimeModule.forRoot(options?)` - -Synchronously configures the module with response time options. - -**Parameters:** - -- `options` (optional): `ResponseTimeOptions` - Configuration options from - `response-time` package - - `digits` (optional): Number of decimal places (default: 3) - - `header` (optional): Header name (default: 'X-Response-Time') - - `suffix` (optional): Boolean to include 'ms' suffix (default: true) - - `custom` (optional): Function to format the time value - -**Returns:** `DynamicModule` - A configured NestJS module - -##### `ResponseTimeModule.forRootAsync(options)` - -Asynchronously configures the module. - -**Parameters:** - -- `options`: Object with one of: - - `useFactory`: - `(...args: any[]) => ResponseTimeOptions | Promise` - - `useClass`: Constructor class - - `useExisting`: Token for existing provider - - `imports` (optional): Array of modules - - `inject` (optional): Array of injection tokens - -**Returns:** `DynamicModule` - A configured NestJS module - -## Usage Examples - -### Basic Setup - -```ts -import { Module } from '@nestjs/common'; -import { ResponseTimeModule } from '@rineex/response-time-mw-module'; - -@Module({ - imports: [ResponseTimeModule.forRoot()], -}) -export class AppModule {} -``` - -### With Custom Header Name - -```ts -import { Module } from '@nestjs/common'; -import { ResponseTimeModule } from '@rineex/response-time-mw-module'; - -@Module({ - imports: [ - ResponseTimeModule.forRoot({ - header: 'X-Process-Time', - }), - ], -}) -export class AppModule {} -``` - -### Async Configuration - -```ts -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { ResponseTimeModule } from '@rineex/response-time-mw-module'; - -@Module({ - imports: [ - ConfigModule.forRoot(), - ResponseTimeModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (config: ConfigService) => ({ - header: config.get('RESPONSE_TIME_HEADER', 'X-Response-Time'), - digits: config.get('RESPONSE_TIME_DIGITS', 3), - }), - inject: [ConfigService], - }), - ], -}) -export class AppModule {} +```bash +pnpm add @rineex/response-time-mw-module response-time @nestjs/common @nestjs/core express ``` -## Behavior & Guarantees - -### Invariants - -- Middleware applies to all routes (`forRoutes('*')`) -- Response time is measured from middleware execution start to response finish -- Header is set on all HTTP responses (including errors) -- Time is measured in milliseconds with configurable precision - -### Performance - -- Minimal overhead: uses `process.hrtime()` for high-resolution timing -- Header addition is synchronous and fast -- No external dependencies or network calls - -### Concurrency - -- Stateless middleware; safe for concurrent requests -- Each request measures its own response time independently -- No shared state between requests - -## Operational Notes - -### Configuration - -Default behavior: - -- Header name: `X-Response-Time` -- Format: `123.456ms` (with suffix) -- Precision: 3 decimal places - -### Header Format - -The response time header format depends on options: - -- With suffix: `X-Response-Time: 123.456ms` -- Without suffix: `X-Response-Time: 123.456` -- Custom format: Use `custom` function option - -### Common Pitfalls - -1. **Timing accuracy**: Response time includes middleware execution time, not - just route handler time -2. **Header visibility**: Headers are visible to clients; ensure this is - intended -3. **Precision**: Higher precision (more digits) increases header size slightly -4. **Custom format**: Custom formatting functions should be fast to avoid - affecting response time - -### Logging - -- No logging provided by this module -- Response time is only exposed via HTTP headers -- For logging, use middleware that reads the header value - -## License +Import `ResponseTimeModule.register(options)` or `registerAsync`. Supported +options are `digits`, `header`, and `suffix`. The package exports +`ResponseTimeModule`, `ResponseTimeMiddleware`, and `ResponseTimeOptions`. -Apache-2.0 +See [NestJS integrations](../../../docs/nestjs-integrations.md) for the shared +middleware pattern. diff --git a/packages/pg-slonik/README.md b/packages/pg-slonik/README.md index 5e6fa50..0c352a6 100644 --- a/packages/pg-slonik/README.md +++ b/packages/pg-slonik/README.md @@ -1,573 +1,15 @@ # @rineex/pg-slonik -> NestJS module for PostgreSQL using Slonik. Provides type-safe database -> connection management with dependency injection, automatic retry logic, and -> graceful shutdown handling. - -> See also: [API Reference](./docs/API.md) for formal type and decorator -> documentation. - -[![npm version](https://img.shields.io/npm/v/@rineex/pg-slonik)](https://www.npmjs.com/package/@rineex/pg-slonik) -[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue.svg)](https://www.typescriptlang.org/) - -## Table of Contents - -- [Overview](#overview) -- [Installation](#installation) - - [Slonik version and third-party interceptors](#slonik-version-and-third-party-interceptors) -- [Quick Start](#quick-start) -- [API Reference](#api-reference) -- [Examples](#examples) -- [Best Practices](#best-practices) -- [Contributing](#contributing) -- [License](#license) - -## Overview - -`@rineex/pg-slonik` is a NestJS module wrapper for -[Slonik](https://github.com/gajus/slonik), providing seamless integration with -NestJS dependency injection, automatic connection retry logic, and graceful -shutdown handling. - -### Key Features - -- **NestJS Integration**: Full NestJS module support with dependency injection -- **Multiple Connections**: Support for multiple named database connections -- **Type-Safe Database Access**: Full TypeScript support with Slonik's type - system -- **Automatic Retry Logic**: Built-in connection retry with configurable - attempts and delays -- **Graceful Shutdown**: Automatic pool cleanup on application shutdown -- **SQL Safety**: Built-in SQL injection protection via Slonik -- **Transaction Support**: First-class transaction handling -- **Production Ready**: Used in high-performance systems at scale - -## Installation - -Install the package and its **peer dependencies** (slonik and NestJS). Using the -same major version of slonik as this package avoids type errors with -interceptors (see -[Slonik version and third-party interceptors](#slonik-version-and-third-party-interceptors)). - -```bash -# npm -npm install @rineex/pg-slonik slonik@^48 @nestjs/common@^11 - -# pnpm -pnpm add @rineex/pg-slonik slonik@^48 @nestjs/common@^11 - -# yarn -yarn add @rineex/pg-slonik slonik@^48 @nestjs/common@^11 -``` - -If you already have `@nestjs/common` and `slonik` in your app, ensure they -satisfy the peer ranges (`@nestjs/common` ^11.0.0, `slonik` ^48.0.0). Your -package manager will warn if versions are incompatible. - -### Requirements - -- **Node.js**: 18.0 or higher -- **TypeScript**: 5.0 or higher (recommended: 5.9+) -- **PostgreSQL**: 12.0 or higher -- **@nestjs/common**: 11.0 or higher -- **slonik**: ^48.0.0 (this package pins slonik 48.x) - -### Slonik version and third-party interceptors - -This package depends on **slonik ^48**. If you use third-party interceptors -(e.g. -[slonik-interceptor-field-name-transformation](https://www.npmjs.com/package/slonik-interceptor-field-name-transformation)), -they **must** target the same slonik major version. - -If your app or another dependency pulls in a different slonik major (e.g. 39.x), -you will see a TypeScript error like: - -```text -Type 'Interceptor' is not assignable to type 'Interceptor'. - Types of property 'afterPoolConnection' are incompatible. - ... - Type 'QuerySqlToken' is not assignable to type 'QuerySqlToken>'. -``` - -That happens because `Interceptor` and `CommonQueryMethods` changed between -slonik 39 (Zod-based) and 48 (different schema types). Two copies of slonik -means two incompatible `Interceptor` types. - -**Fix:** Use a single slonik 48 everywhere. Install slonik and any slonik-based -interceptors with the same major as this package: - -```bash -pnpm add slonik@^48 slonik-interceptor-field-name-transformation@^48 -``` - -In a pnpm/yarn workspace, ensure the app that uses `@rineex/pg-slonik` and the -interceptors resolves to one slonik 48 (e.g. list the example app in the -workspace and use `slonik: "^48.0.0"` in that app’s `package.json` so the -lockfile deduplicates to a single version). - -## Quick Start - -Here's a minimal example to get started with NestJS: - -```typescript -import { Module } from '@nestjs/common'; -import { SlonikModule } from '@rineex/pg-slonik'; - -@Module({ - imports: [ - SlonikModule.register({ - connections: [ - { - name: 'DEFAULT', - dsn: 'postgresql://user:password@localhost:5432/database', - }, - ], - }), - ], -}) -export class AppModule {} -``` - -Then inject the pool in your service: - -```typescript -import { Injectable } from '@nestjs/common'; -import { InjectPool } from '@rineex/pg-slonik'; -import type { DatabasePool } from 'slonik'; - -@Injectable() -export class UserService { - constructor(@InjectPool() private readonly pool: DatabasePool) {} - - async findUser(id: string) { - const result = await this.pool.query( - this.pool.sql`SELECT * FROM users WHERE id = ${id}`, - ); - return result.rows[0]; - } -} -``` - -## API Reference - -### `SlonikModule` - -NestJS module for managing Slonik database connections. - -#### `SlonikModule.register(options: SlonikModuleOptions): DynamicModule` - -Registers the Slonik module with the provided configuration. - -**Parameters:** - -- `options.connections` (required): Array of connection configurations - - `connection.name` (required): Unique name for the connection (default: - 'DEFAULT') - - `connection.dsn` (required): PostgreSQL connection string (DSN) - - `connection.options` (optional): Slonik `ClientConfigurationInput` options - - `connection.tags` (optional): Array of connection tags -- `options.isGlobal` (optional): Whether the module should be global (default: - true) - -**Returns:** A configured NestJS `DynamicModule` - -### `InjectPool(name?: string)` - -Decorator for injecting a Slonik database pool into a class. - -**Parameters:** - -- `name` (optional): Name of the connection to inject (default: 'DEFAULT') - -**Returns:** A NestJS `Inject` decorator - -**Example:** - -```typescript -@Injectable() -export class MyService { - constructor(@InjectPool('DEFAULT') private readonly pool: DatabasePool) {} -} -``` - -### Type Exports - -- `SlonikConnectionConfig`: Configuration interface for a single connection -- `SlonikModuleOptions`: Module configuration interface -- `SlonikModuleExtraOptions`: Additional module options (e.g., `isGlobal`) - -## Examples - -### Basic Module Setup - -```typescript -import { Module } from '@nestjs/common'; -import { SlonikModule } from '@rineex/pg-slonik'; - -@Module({ - imports: [ - SlonikModule.register({ - connections: [ - { - name: 'DEFAULT', - dsn: process.env.DATABASE_URL!, - }, - ], - }), - ], -}) -export class AppModule {} -``` - -### Multiple Connections - -```typescript -import { Module } from '@nestjs/common'; -import { SlonikModule } from '@rineex/pg-slonik'; - -@Module({ - imports: [ - SlonikModule.register({ - connections: [ - { - name: 'DEFAULT', - dsn: process.env.DATABASE_URL!, - }, - { - name: 'ANALYTICS', - dsn: process.env.ANALYTICS_DATABASE_URL!, - options: { - maximumPoolSize: 5, - statementTimeout: 30000, - }, - }, - ], - }), - ], -}) -export class AppModule {} -``` - -### Service with Dependency Injection - -```typescript -import { Injectable } from '@nestjs/common'; -import { InjectPool } from '@rineex/pg-slonik'; -import type { DatabasePool } from 'slonik'; - -@Injectable() -export class UserService { - constructor(@InjectPool() private readonly pool: DatabasePool) {} - - async findAll() { - const result = await this.pool.query( - this.pool.sql`SELECT id, email FROM users LIMIT 10`, - ); - return result.rows; - } - - async findById(id: string) { - const result = await this.pool.query( - this.pool.sql`SELECT * FROM users WHERE id = ${id}`, - ); - return result.rows[0] || null; - } -} -``` - -### Using Named Connections - -```typescript -import { Injectable } from '@nestjs/common'; -import { InjectPool } from '@rineex/pg-slonik'; -import type { DatabasePool } from 'slonik'; - -@Injectable() -export class AnalyticsService { - constructor( - @InjectPool('ANALYTICS') private readonly analyticsPool: DatabasePool, - ) {} - - async getMetrics() { - const result = await this.analyticsPool.query( - this.analyticsPool.sql`SELECT * FROM metrics ORDER BY created_at DESC`, - ); - return result.rows; - } -} -``` - -### Transaction Handling - -```typescript -import { Injectable } from '@nestjs/common'; -import { InjectPool } from '@rineex/pg-slonik'; -import type { DatabasePool } from 'slonik'; - -@Injectable() -export class UserService { - constructor(@InjectPool() private readonly pool: DatabasePool) {} - - async createUserWithProfile(userId: string, email: string, name: string) { - await this.pool.transaction(async transactionConnection => { - await transactionConnection.query( - transactionConnection.sql` - INSERT INTO users (id, email) VALUES (${userId}, ${email}) - `, - ); - - await transactionConnection.query( - transactionConnection.sql` - INSERT INTO user_profiles (user_id, name) VALUES (${userId}, ${name}) - `, - ); - }); - } -} -``` - -### Repository Pattern - -```typescript -import { Injectable } from '@nestjs/common'; -import { InjectPool } from '@rineex/pg-slonik'; -import type { DatabasePool } from 'slonik'; - -interface User { - id: string; - email: string; - createdAt: Date; -} - -@Injectable() -export class UserRepository { - constructor(@InjectPool() private readonly pool: DatabasePool) {} - - async findById(id: string) { - const result = await this.pool.query( - this.pool.sql` - SELECT * FROM users WHERE id = ${id} - `, - ); - - if (result.rows.length === 0) { - return null; - } - - // Map database row to domain object - return this.mapToUser(result.rows[0]); - } - - async save(user: User) { - await this.pool.query( - this.pool.sql` - INSERT INTO users (id, email, created_at) - VALUES (${user.id}, ${user.email}, ${user.createdAt}) - ON CONFLICT (id) DO UPDATE - SET email = ${user.email} - `, - ); - } - - private mapToUser(row: any): User { - return { - id: row.id, - email: row.email, - createdAt: row.created_at, - }; - } -} -``` - -### Advanced Configuration - -```typescript -import { Module } from '@nestjs/common'; -import { SlonikModule } from '@rineex/pg-slonik'; - -@Module({ - imports: [ - SlonikModule.register({ - connections: [ - { - name: 'DEFAULT', - dsn: process.env.DATABASE_URL!, - options: { - maximumPoolSize: 20, - idleTimeout: 5000, - statementTimeout: 60000, - connectionTimeout: 10000, - interceptors: [ - // Add custom interceptors here - ], - }, - tags: ['primary', 'read-write'], - }, - ], - isGlobal: true, // Make the module global - }), - ], -}) -export class AppModule {} -``` - -## Best Practices - -### 1. **Use Dependency Injection** - -Always inject the pool using the `@InjectPool()` decorator instead of creating -pools manually: - -```typescript -// ❌ BAD: Creating pools manually -const pool = await createPool(connectionString); - -// ✅ GOOD: Using dependency injection -@Injectable() -export class MyService { - constructor(@InjectPool() private readonly pool: DatabasePool) {} -} -``` - -### 2. **Automatic Pool Cleanup** - -The module automatically closes all connection pools on application shutdown. -You don't need to manually handle cleanup: - -```typescript -// ✅ GOOD: Automatic cleanup via OnApplicationShutdown -// The SlonikModule handles this automatically -``` - -### 3. **Use Tagged Template Literals** - -Always use Slonik's `sql` tagged template literal for queries to prevent SQL -injection: - -```typescript -// ❌ BAD: String concatenation (SQL injection risk) -const query = `SELECT * FROM users WHERE id = '${userId}'`; - -// ✅ GOOD: Tagged template literal -const query = this.pool.sql`SELECT * FROM users WHERE id = ${userId}`; -``` - -### 4. **Handle Transactions Properly** - -Use transactions for operations that must be atomic: - -```typescript -await this.pool.transaction(async transactionConnection => { - // All queries in this block are part of the same transaction - await transactionConnection.query(/* ... */); - await transactionConnection.query(/* ... */); - // Transaction commits automatically if no errors -}); -``` - -### 5. **Map Database Rows to Domain Objects** - -Keep database concerns separate from domain logic: - -```typescript -// ❌ BAD: Exposing database rows directly -async getUser(id: string) { - const result = await this.pool.query(/* ... */); - return result.rows[0]; // Raw database row -} - -// ✅ GOOD: Mapping to domain objects -async getUser(id: string) { - const result = await this.pool.query(/* ... */); - return this.mapToUser(result.rows[0]); // Domain object -} -``` - -### 6. **Use Named Connections for Multiple Databases** - -When working with multiple databases, use named connections: - -```typescript -// ✅ GOOD: Named connections for different databases -SlonikModule.register({ - connections: [ - { name: 'DEFAULT', dsn: process.env.DATABASE_URL! }, - { name: 'ANALYTICS', dsn: process.env.ANALYTICS_DB_URL! }, - ], -}); - -// Then inject the specific connection -@InjectPool('ANALYTICS') private readonly analyticsPool: DatabasePool -``` - -### 7. **Configure Connection Options** - -Use the `options` field to configure pool behavior: - -```typescript -SlonikModule.register({ - connections: [ - { - name: 'DEFAULT', - dsn: process.env.DATABASE_URL!, - options: { - maximumPoolSize: 20, - statementTimeout: 60000, - // Add other Slonik configuration options - }, - }, - ], -}); -``` - -## Contributing - -Contributions are welcome! Please follow these guidelines: - -1. **Fork** the repository -2. **Create** a feature branch (`git checkout -b feature/amazing-feature`) -3. **Write** tests for new functionality -4. **Ensure** all tests pass (`pnpm test`) -5. **Follow** the code style (`pnpm lint`) -6. **Commit** with clear messages -7. **Push** to the branch and create a Pull Request - -### Development Setup +NestJS integration for named Slonik PostgreSQL pools. Pools are verified with +retries, injectable by name, and closed on application shutdown. ```bash -# Install dependencies -pnpm install - -# Run tests -pnpm test - -# Run linter -pnpm lint - -# Check types -pnpm check-types - -# Build the package -pnpm build +pnpm add @rineex/pg-slonik slonik @nestjs/common ``` -## License - -This project is licensed under the Apache License 2.0 - see the -[LICENSE](../../LICENSE) file for details. - -## Related Resources - -- [Slonik Documentation](https://github.com/gajus/slonik) -- [PostgreSQL Documentation](https://www.postgresql.org/docs/) -- [NestJS Documentation](https://docs.nestjs.com/) - -## Support - -For issues, questions, or suggestions, please open an issue on -[GitHub](https://github.com/rineex/core/issues). - ---- +Call `SlonikModule.register({ connections })`; each connection has a name, +PostgreSQL DSN, and optional Slonik options. Use `@InjectPool()` for `DEFAULT` +or `@InjectPool('replica')` for another configured name. -**Made with ❤️ by the Rineex Team** +See [NestJS integrations](../../docs/nestjs-integrations.md) for a full module +example. diff --git a/packages/pg-slonik/docs/API.md b/packages/pg-slonik/docs/API.md deleted file mode 100644 index 1dd3e44..0000000 --- a/packages/pg-slonik/docs/API.md +++ /dev/null @@ -1,194 +0,0 @@ -# @rineex/pg-slonik — API Documentation - -> [← Back to package README](../README.md) - -## 1. Overview - -**What it does:** NestJS module that provides one or more -[Slonik](https://github.com/gajus/slonik) PostgreSQL connection pools as -injectable providers. Each connection is created with configurable DSN, optional -Slonik client options (including interceptors), and connection verification with -retries. - -**Use when:** - -- You need type-safe PostgreSQL access via Slonik in a NestJS app. -- You want multiple named pools (e.g. primary vs replica) or a single default - pool. -- You need Slonik features (interceptors, type parsers, statement timeouts) - passed through per connection. - -**Do not use when:** - -- You are not using NestJS. -- You need a different PostgreSQL client (e.g. raw `pg`, Prisma). - ---- - -## 2. Public API - -### Types (`slonik.interface.ts`) - -| Symbol | Description | -| -------------------------- | -------------------------------------------------------------------------------------------------------------- | -| `SlonikConnectionName` | `string` — Name of the connection (used for injection token and logging). | -| `SlonikConnectionTags` | `string` — Tag type for connection metadata. | -| `SlonikConnectionConfig` | Per-connection config: `name`, `dsn`, optional `options` (Slonik `ClientConfigurationInput`), optional `tags`. | -| `SlonikModuleOptions` | `{ connections: SlonikConnectionConfig[] }` — Required for `SlonikModule.register()`. | -| `SlonikModuleExtraOptions` | `{ isGlobal?: boolean }` — Default `isGlobal: true`; controls module scope. | - -### Module (`slonik.module.ts`) - -| API | Description | -| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `SlonikModule.register(options)` | Registers the module with the given connections. Returns a `DynamicModule` with one provider per connection and exports all pool tokens. | -| `SlonikModule#onApplicationShutdown()` | Ends all pools created by this module (called by Nest on shutdown). | - -**Parameters (register):** - -- `options.connections` (required): Array of `SlonikConnectionConfig`. -- `options.isGlobal` (optional): If `true`, module is global (default from - builder). - -**Return (register):** NestJS `DynamicModule` with `providers` and `exports` -set. Each connection is provided under the token returned by -`createSlonikToken(conn.name)`. - -### Utilities (`slonik.util.ts`) - -| Function | Parameters | Return | Description | -| -------------------------------- | ------------------------------------------------------------------------------------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `createSlonikToken(name?)` | `name` (optional, default `'DEFAULT'`) | `string` | DI token for the pool, e.g. `__SLONIK_DEFAULT_POOL__`. Name is uppercased. | -| `createSlonikConnection(config)` | `config: SlonikConnectionConfig` | `Promise` | Creates a Slonik pool via `createPool(dsn, options)`, verifies with `pool.connect()`, retries on failure (see `handleRetry`). | -| `handleRetry(...)` | `retryAttempts=9`, `retryDelay=3000`, `poolName`, `verboseRetryLog`, `toRetry?(err)` | RxJS operator | Retry operator for connection failures. If `toRetry` is provided, retries only when it returns `true`. | - -**Errors:** `createSlonikConnection` rejects if the pool cannot be created or -the verification connect fails after all retries. Errors are logged with -`Logger` (SlonikModule). - -### Decorator (`slonik.decorator.ts`) - -| API | Parameters | Return | Description | -| ------------------- | -------------------------------------- | ------------------- | ---------------------------------------------------------------- | -| `InjectPool(name?)` | `name` (optional, default `'DEFAULT'`) | Parameter decorator | Injects the Slonik `DatabasePool` for the given connection name. | - -### Module definition (`slonik.module-definition.ts`) - -| Export | Description | -| ---------------------- | ------------------------------------------------------------ | -| `SlonikCoreModule` | Base configurable module class. | -| `MODULE_OPTIONS_TOKEN` | Token to inject `SlonikModuleOptions` (with extras applied). | -| `OPTIONS_TYPE` | Type of options for `register()`. | -| `ASYNC_OPTIONS_TYPE` | Type for async registration. | - -### Constants (`constants/index.ts`) - -| Constant | Value | Description | -| ------------------- | ---------------------- | ----------------------------------------------- | -| `SLONIK_POOL_TOKEN` | `'__SLONIK_%s_POOL__'` | Format string for pool injection token. | -| `DEFAULT_POOL_NAME` | `'DEFAULT'` | Default connection name when none is specified. | - ---- - -## 3. Usage Examples - -**Single default pool:** - -```ts -import { Module } from '@nestjs/common'; -import { SlonikModule } from '@rineex/pg-slonik'; - -@Module({ - imports: [ - SlonikModule.register({ - connections: [{ name: 'DEFAULT', dsn: process.env.DATABASE_URL! }], - }), - ], -}) -export class AppModule {} -``` - -**Multiple pools with options and interceptors:** - -```ts -SlonikModule.register({ - connections: [ - { - name: 'primary', - dsn: process.env.DATABASE_URL!, - options: { - maximumPoolSize: 20, - statementTimeout: 60_000, - interceptors: [myQueryLoggingInterceptor], - }, - }, - { - name: 'replica', - dsn: process.env.DATABASE_REPLICA_URL!, - options: { interceptors: [readOnlyInterceptor] }, - }, - ], -}); -``` - -**Injecting the pool:** - -```ts -import { InjectPool } from '@rineex/pg-slonik'; -import type { DatabasePool } from 'slonik'; - -@Injectable() -export class UserRepository { - constructor( - @InjectPool() private readonly pool: DatabasePool, - // or named: @InjectPool('replica') private readonly replica: DatabasePool, - ) {} -} -``` - -**Async registration (e.g. from ConfigService):** - -```ts -SlonikModule.registerAsync({ - inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - connections: [{ name: 'DEFAULT', dsn: config.getOrThrow('DATABASE_URL') }], - }), -}); -``` - ---- - -## 4. Behavior & Guarantees - -- **Pool lifecycle:** Pools are created when the provider is first resolved - (lazy). They are closed on `onApplicationShutdown()` via `pool.end()`; - shutdown uses `Promise.allSettled` so one failure does not block others. -- **Retries:** Connection creation is wrapped in an RxJS `defer` + - `handleRetry()`. Default: 9 retries, 3s delay. Only the initial - connection/verification is retried; subsequent query failures are not retried - by this module. -- **Token format:** Injection token is `__SLONIK__POOL__`. - Connection names are case-insensitive for token creation (normalized to - uppercase). -- **Options passthrough:** `SlonikConnectionConfig.options` is passed directly - to Slonik’s `createPool(dsn, options)`. All Slonik options (e.g. - `interceptors`, `typeParsers`, `statementTimeout`) are supported. - ---- - -## 5. Operational Notes - -- **Configuration:** Use environment variables or a config module for DSN and - secrets; avoid hardcoding. For multiple connections, use distinct env vars and - names. -- **Logging:** The module uses Nest `Logger` (scope `SlonikModule`) for - connection success and retry errors. Enable Slonik’s own logging via - `interceptors` or client options if you need query-level logs. -- **Pitfalls:** - - Registering the same connection name twice in one app can lead to duplicate - tokens; keep names unique per process. - - `handleRetry` is applied only to the initial `createPool` + `connect`; - ensure DSN and network are correct. - - If you use `registerAsync`, ensure the factory returns the same shape as - `register()` (e.g. `connections` array). diff --git a/packages/pg-slonik/example/README.md b/packages/pg-slonik/example/README.md index 5a09fe4..41f9a9d 100644 --- a/packages/pg-slonik/example/README.md +++ b/packages/pg-slonik/example/README.md @@ -1,47 +1,13 @@ -# @rineex/pg-slonik example +# pg-slonik example -Minimal NestJS app that uses `@rineex/pg-slonik` with -[slonik-interceptor-field-name-transformation](https://www.npmjs.com/package/slonik-interceptor-field-name-transformation) -to inject a Slonik pool and run queries. This folder is **not** part of the pnpm -workspace; it is for developers to see how to use the package. +A private NestJS application demonstrating `@rineex/pg-slonik` registration with +a local PostgreSQL DSN and a Slonik field-name transformation interceptor. -## Setup +```bash +pnpm install +pnpm --dir packages/pg-slonik/example start:dev +``` -1. Build the package from the monorepo root: - - ```bash - pnpm --filter @rineex/pg-slonik build - ``` - -2. Install and run from this directory (use `--ignore-workspace` so this - folder’s dependencies are installed): - - ```bash - cd packages/pg-slonik/example - pnpm install --ignore-workspace - pnpm build - pnpm start - ``` - -Optional: set `DATABASE_URL` (default: -`postgresql://rineex:rineex@localhost:5432/rineex`). - -## Endpoints - -- **GET /** – Welcome message -- **GET /health** – Runs `SELECT current_timestamp` via Slonik and returns - `{ ok, now }` -- **GET /fields** – Creates a table with snake_case columns, inserts a row, and - returns rows so you can verify the interceptor returns camelCase keys - (`fullName`, `createdAt`) - -## Usage in code - -- `SlonikModule.register({ connections: [{ name: 'DEFAULT', dsn, options: { interceptors } }] })` - in `app.module.ts` -- Add `createFieldNameTransformationInterceptor({ format: 'CAMEL_CASE' })` from - `slonik-interceptor-field-name-transformation` to transform result field names - to camelCase -- `@InjectPool()` in services to inject the default pool -- Use `sql` from `slonik` (e.g. `sql.unsafe` for literal SQL) and - `this.pool.one()`, `this.pool.any()`, etc. +The example source uses `postgresql://rineex:rineex@localhost:5432/rineex`; make +that database available or replace the DSN in `src/app.module.ts` before +starting it. diff --git a/packages/typescript-config/README.md b/packages/typescript-config/README.md new file mode 100644 index 0000000..928c847 --- /dev/null +++ b/packages/typescript-config/README.md @@ -0,0 +1,8 @@ +# @rineex/typescript-config + +Private shared TypeScript configuration for this workspace. + +`base.json` enables strict, bundler-oriented TypeScript defaults. +`react-library.json` extends it for React libraries with DOM and JSX support. +Consume these files from workspace package `tsconfig.json` files; this package +does not provide a runtime API. diff --git a/plan/feature-decision-engine-1.md b/plan/feature-decision-engine-1.md deleted file mode 100644 index 3fd327d..0000000 --- a/plan/feature-decision-engine-1.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -goal: Deliver a usable, framework-agnostic decision execution engine -version: 1.0 -date_created: 2026-10-01 -last_updated: 2026-10-01 -owner: Rineex Team -status: Completed -tags: [feature, decision-engine, domain, validation] ---- - -# Introduction - -![Status: Completed](https://img.shields.io/badge/status-Completed-brightgreen) - -`@rineex/decision-engine` will be completed as a real decision-execution -library, not retained as a placeholder. The package already contains a 911-line -execution pipeline and contracts for constraints, features, normalization, -scoring, ranking, selection, and events. The completed work makes the intended -engine internally consistent, executable, exported, and verified through -end-to-end and adversarial tests. - -## 1. Requirements & Constraints - -- **REQ-001**: The published package must expose a usable `DecisionEngine`, - `DefaultDecisionValidator`, built-in strategies, domain contracts, and errors - from `packages/decision-engine/src/index.ts`. -- **REQ-002**: One canonical `DecisionDefinition` must include - `candidateRefResolver`; remove the duplicate definition currently placed in - `src/domain/event/decision-completed-event.ts`. -- **REQ-003**: A feature value must carry the feature objective required by - `DecisionEngine.evaluateFeatures` and `MinMaxNormalizer`. -- **REQ-004**: All built-in strategies must validate their inputs, avoid - mutation, and either return valid results or throw a decision-domain error. -- **REQ-005**: `DecisionExecution.result`, evaluations, event collections, and - built-in strategy outputs must be immutable from consumers' perspective. -- **REQ-006**: The engine must preserve input candidate order for evaluation; - descending-score ties must preserve that order. -- **REQ-007**: Candidate references must be non-empty, unique strings for one - execution; selected candidates must be a subset of ranked eligible candidates. -- **REQ-008**: The default validator must reject malformed definitions and - requests before candidate evaluation begins. -- **CON-001**: Keep the engine synchronous and framework-independent. Event - publishing stays an opt-in infrastructure adapter and is not called by - `DecisionEngine.execute`. -- **CON-002**: Do not publish the package until `check-types`, `test`, `lint`, - and `build` pass. -- **PAT-001**: Apply the pipeline in this order: validate, constrain, extract, - normalize, score, rank, select, build result, create events. - -## 2. Implementation Steps - -### Implementation Phase 1 - -- GOAL-001: Establish one coherent public decision model and a compiling API. - -| Task | Description | Completed | Date | -| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ---- | -| TASK-001 | Update `packages/decision-engine/src/domain/model/decision-definition.ts` to import `CandidateRefResolver` and add required `candidateRefResolver: CandidateRefResolver`. | | | -| TASK-002 | Replace the erroneous `DecisionDefinition` in `packages/decision-engine/src/domain/event/decision-completed-event.ts` with a `DecisionCompletedEventPayload` and `DecisionCompletedEvent` contract matching `DecisionEngine.createDecisionEvents`; remove its unrelated strategy imports. | | | -| TASK-003 | Update `packages/decision-engine/src/domain/feature/feature-value.ts` to include `objective: FeatureObjective`; update the feature interface and all tests/fixtures so feature evaluation returns `{ key, objective, rawValue }`. | | | -| TASK-004 | Change `DecisionEvent`'s default payload type to `unknown` (or a covariant object-safe equivalent) and update `DecisionExecution` plus event publisher contracts so `CandidateRejectedEvent` and `DecisionCompletedEvent` are assignable without casts. | | | -| TASK-005 | Implement constructors in `src/domain/error/decision-error.ts`, `decision-execution-error.ts`, and `invalid-decision-definition-error.ts`; call `super(message, { cause })`, set stable names, and retain `details`. | | | -| TASK-006 | Implement `DefaultDecisionValidator` in `src/application/default-decision-validator.ts` against the canonical model import. Validate non-empty IDs/versions, arrays, unique feature/constraint IDs, strategy objects and IDs, resolver presence, candidates array, policy ID/version, and optional correlation ID. | | | -| TASK-007 | Update `src/index.ts` to export only the supported public API: engine, validator, built-in strategies, event publisher, contracts, objectives, and errors. Do not export the legacy descriptor helper unless it is redefined as an engine factory with documented behavior. | | | - -### Implementation Phase 2 - -- GOAL-002: Implement deterministic built-in decision behavior. - -| Task | Description | Completed | Date | -| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---- | -| TASK-008 | Implement `MinMaxNormalizer.normalize` in `src/domain/normalization/min-max-normalizer.ts`: validate equal feature key/objective sets across eligible evaluations; normalize maximize features as `(value-min)/(max-min)` and minimize features as `(max-value)/(max-min)`; assign `1` to every candidate when a feature has zero range; return copied evaluations and copied feature values. | | | -| TASK-009 | Implement `WeightedSumScoringStrategy.score` in `src/domain/scoring/weighted-sum-scoring-strategy.ts`: require finite normalized values and finite resolver weights, then return the finite sum of `normalizedValue * weight`; wrap resolver errors as `DecisionExecutionError` with feature metadata. | | | -| TASK-010 | Implement `DescendingScoreRankingStrategy.rank` in `src/domain/scoring/descending-score-ranking-strategy.ts`: reject ineligible/missing/non-finite scores, return a copied stable descending ordering, and leave evaluation objects unchanged. | | | -| TASK-011 | Implement `SelectFirstStrategy.select`, `SelectTopNStrategy`, and `SelectAboveThresholdStrategy` under `src/domain/selection/`: validate constructor parameters, reject invalid ranking inputs where necessary, and return copied subsets without mutating input. | | | -| TASK-012 | Implement `InMemoryDecisionEventPublisher` in `src/infrastructure/event/in-memory-decision-event-publisher.ts` as a concrete class: initialize handlers, reject blank event types, register handlers in order, and publish events/handlers sequentially. | | | - -### Implementation Phase 3 - -- GOAL-003: Prove the engine through black-box execution tests and package - gates. - -| Task | Description | Completed | Date | -| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---- | -| TASK-013 | Replace `src/decision-engine.spec.ts`'s descriptor-only tests with end-to-end `DecisionEngine.execute` tests using a small candidate fixture. Cover constraints, maximize and minimize features, weighted scoring, descending ranking, top-N selection, selected flags, result metadata, correlation ID, rejection event, and completion event. | | | -| TASK-014 | Add focused specs beside the implementations for zero-range normalization, inconsistent feature output, non-finite feature/weight/score values, ranking ties, invalid limits/thresholds, duplicate/blank candidate references, selector attempts to return rejected or unknown candidates, and immutability/non-mutation guarantees. | | | -| TASK-015 | Add `InMemoryDecisionEventPublisher` tests for subscription order, event order, blank event type rejection, and handler failure propagation. | | | -| TASK-016 | Run `pnpm --filter @rineex/decision-engine check-types`, `test`, `lint`, and `build`. Fix every failure introduced by this package. Confirm the built declarations expose `DecisionEngine` and one end-to-end test imports it from the package root. | | | - -## 3. Alternatives - -- **ALT-001**: Delete all unfinished internals and preserve only - `initDecisionEngine`. Rejected because the WIP commit deliberately introduced - the full pipeline and its contracts, making a descriptor-only package - misleading and needlessly maintaining dead code. -- **ALT-002**: Export the existing pipeline before implementing it. Rejected - because it fails typechecking and exposes unusable runtime declarations. -- **ALT-003**: Build a rule DSL, persistence integration, or asynchronous - execution in this effort. Rejected because they are outside the current - synchronous, framework-agnostic model. - -## 4. Dependencies - -- **DEP-001**: TypeScript 5.9, Vitest 4, ESLint, and tsup already declared in - `packages/decision-engine/package.json`. -- **DEP-002**: No new production dependency is required. - -## 5. Files - -- **FILE-001**: `packages/decision-engine/src/index.ts` -- **FILE-002**: `packages/decision-engine/src/application/decision-engine.ts` -- **FILE-003**: - `packages/decision-engine/src/application/default-decision-validator.ts` -- **FILE-004**: - `packages/decision-engine/src/domain/model/decision-definition.ts`, - `decision-event.ts`, and `decision-execution.ts` -- **FILE-005**: - `packages/decision-engine/src/domain/event/decision-completed-event.ts` and - `candidate-rejected-event.ts` -- **FILE-006**: `packages/decision-engine/src/domain/error/*.ts` -- **FILE-007**: `packages/decision-engine/src/domain/feature/feature-value.ts` -- **FILE-008**: - `packages/decision-engine/src/domain/normalization/min-max-normalizer.ts` -- **FILE-009**: `packages/decision-engine/src/domain/scoring/*.ts` and - `src/domain/selection/*.ts` -- **FILE-010**: - `packages/decision-engine/src/infrastructure/event/in-memory-decision-event-publisher.ts` -- **FILE-011**: Unit and end-to-end specs under `packages/decision-engine/src/` -- **FILE-012**: `packages/decision-engine/README.md` - -## 6. Testing - -- **TEST-001**: A two-candidate execution normalizes a maximize and minimize - feature, produces expected weighted scores, ranks deterministically, and - selects the top candidate. -- **TEST-002**: A rejected candidate has no features, score, rank, or selection - and produces a complete rejection event. -- **TEST-003**: Invalid contracts fail before evaluation, and invalid strategy - output fails with a structured decision error. -- **TEST-004**: Equal raw values normalize to `1`; equal scores preserve input - order. -- **TEST-005**: Inputs and returned intermediate collections are never mutated. -- **TEST-006**: Root-package imports compile and execute; typecheck, tests, - lint, and build all pass. - -## 7. Risks & Assumptions - -- **RISK-001**: No in-repository callers currently constrain the public API; the - test suite and README must define the initial compatibility contract. -- **RISK-002**: Adding `objective` to `FeatureValue` is a breaking contract - correction, but the package is at `0.1.0` and is not currently functional. -- **RISK-003**: Runtime immutability requires deliberate copied/frozen outputs; - TypeScript `readonly` alone is insufficient. -- **ASSUMPTION-001**: The WIP pipeline reflects the intended product direction. -- **ASSUMPTION-002**: Negative weights are permitted because policy semantics, - rather than the framework, determine their meaning. - -## 8. Related Specifications / Further Reading - -- [Package README](../packages/decision-engine/README.md) -- [Initial publishable package commit](../packages/decision-engine/src/decision-engine.ts) diff --git a/plan/refactor-ddd-primitives-1.md b/plan/refactor-ddd-primitives-1.md deleted file mode 100644 index 949d232..0000000 --- a/plan/refactor-ddd-primitives-1.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -goal: Harden DDD entities, aggregate roots, and domain events -version: 1.0 -date_created: 2026-08-08 -last_updated: 2026-08-08 -owner: Rineex Team -status: Completed -tags: [refactor, ddd, architecture, breaking-change] ---- - -# Introduction - -![Status: Completed](https://img.shields.io/badge/status-Completed-brightgreen) - -Refactor `@rineex/ddd` to enforce safe invariant validation, atomic entity -mutation, aggregate-owned event recording, explicit event metadata, and -mapper-based rehydration. Migrate all in-repository consumers to the new -contract. - -## 1. Requirements & Constraints - -- **REQ-001**: `Entity` must not invoke overridable subclass validation from its - base constructor. -- **REQ-002**: Entity mutation must validate candidate state before committing - it; failed mutation must preserve the previous state. -- **REQ-003**: Entity metadata and exposed state must not permit mutation - through `Date` methods or mutable references. -- **REQ-004**: Concrete entity and aggregate creation/rehydration paths must - validate explicitly. -- **REQ-005**: Aggregate event recording must be protected and reject events - whose aggregate ID differs from the root ID. -- **REQ-006**: Domain events must require explicit ID, stable name, positive - schema version, valid occurrence timestamp, and immutable JSON-safe payload. -- **REQ-007**: The domain event base class must not depend on `node:crypto`. -- **REQ-008**: Rehydration through mappers must not emit creation events. -- **REQ-009**: Update package documentation, migration guidance, tests, and all - in-repository consumers. -- **CON-001**: The change is intentionally breaking; no deprecated compatibility - aliases are required. -- **CON-002**: Preserve dependency direction: infrastructure and application - depend on domain, never the reverse. -- **CON-003**: Keep public API names and examples internally consistent after - migration. -- **PAT-001**: Domain methods express business intent; controllers, - repositories, and event buses remain outside aggregates. - -## 2. Implementation Steps - -### Implementation Phase 1 - -- GOAL-001: Define safe entity state and validation contracts. - -| Task | Description | Completed | Date | -| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---- | -| TASK-001 | Update `packages/ddd/src/domain/entities/entity.ts`: remove constructor-time virtual validation; introduce candidate-props validation; make `mutate` validate before assignment; protect metadata/state snapshots from mutable `Date` references; preserve identity equality and JSON typing. | | | -| TASK-002 | Update `packages/ddd/src/domain/types/deep-immutable.type.ts` and related utilities/types so the compile-time immutability contract matches runtime behavior for dates, arrays, maps, sets, and nested values. | | | -| TASK-003 | Update entity and type tests under `packages/ddd/src/domain/entities/__tests__/` and `packages/ddd/src/domain/types/__tests__/` for explicit construction validation, rollback after failed mutation, and date immutability. | | | - -### Implementation Phase 2 - -- GOAL-002: Harden aggregate boundaries and event collection. - -| Task | Description | Completed | Date | -| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---- | -| TASK-004 | Update `packages/ddd/src/domain/aggregates/aggregate-root.ts`: make event recording protected, verify aggregate identity, preserve ordered pull semantics, and document the application publication boundary. | | | -| TASK-005 | Update `packages/ddd/src/domain/aggregates/__tests__/aggregate-root.spec.ts` for protected recording through a test aggregate, mismatched aggregate IDs, event ordering, and pull behavior. | | | -| TASK-006 | Update all aggregate implementations in `packages/authentication/**/src/domain/**` to validate after construction and replace public `addEvent` calls with the new protected recording API. | | | - -### Implementation Phase 3 - -- GOAL-003: Make domain events explicit, portable, and immutable. - -| Task | Description | Completed | Date | -| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---- | -| TASK-007 | Update `packages/ddd/src/domain/events/domain.event.ts`: remove `node:crypto`, require event ID and occurrence metadata, validate event metadata, and return detached JSON-safe primitives. | | | -| TASK-008 | Migrate event classes and factories under `packages/authentication/**/src/domain/events/` and their tests to provide explicit event IDs/timestamps and stable versioned payloads. | | | -| TASK-009 | Expand `packages/ddd/src/domain/events/__tests__/domain.event.spec.ts` for invalid metadata, nested payload immutability, detached serialization, and runtime portability assumptions. | | | - -### Implementation Phase 4 - -- GOAL-004: Complete mapper/documentation migration and verification. - -| Task | Description | Completed | Date | -| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---- | -| TASK-010 | Update `packages/ddd/src/infrastructure/mapper/base.mapper.ts`, mapper types, and consumer mappers to expose explicit rehydration intent without emitting creation events. | | | -| TASK-011 | Reconcile `packages/ddd/README.md`, `packages/ddd/docs/DDD-GUIDE.md`, and `packages/ddd/docs/MIGRATION.md` with the final API and add minimal creation, command, event, and rehydration examples. | | | -| TASK-012 | Run `pnpm --filter @rineex/ddd check-types`, `test`, `lint`, and `build`; run affected authentication type/tests; fix all failures caused by the migration. | | | - -## 3. Alternatives - -- **ALT-001**: Preserve constructor-time `validate()` for source compatibility. - Rejected because it invokes overridable behavior before subclass - initialization. -- **ALT-002**: Keep public `addEvent()` and trust callers. Rejected because it - allows bypassing aggregate ownership and event invariants. -- **ALT-003**: Keep `node:crypto` event-ID generation. Rejected because it - couples a domain abstraction to one runtime and hides event identity creation. -- **ALT-004**: Use event sourcing or CQRS as part of this refactor. Rejected as - unnecessary complexity; this change only hardens tactical DDD primitives. - -## 4. Dependencies - -- **DEP-001**: TypeScript 5.9 and existing package type definitions. -- **DEP-002**: Existing value-object ID implementations satisfying `EntityId`. -- **DEP-003**: Existing authentication event factories and aggregate - implementations. -- **DEP-004**: Existing Vitest, tsd, ESLint, and tsup verification commands. - -## 5. Files - -- **FILE-001**: `packages/ddd/src/domain/entities/entity.ts` -- **FILE-002**: `packages/ddd/src/domain/aggregates/aggregate-root.ts` -- **FILE-003**: `packages/ddd/src/domain/events/domain.event.ts` -- **FILE-004**: `packages/ddd/src/domain/types/deep-immutable.type.ts` and - related type tests -- **FILE-005**: DDD unit/type tests under - `packages/ddd/src/domain/**/__tests__/` -- **FILE-006**: Authentication aggregate/event consumers under - `packages/authentication/**/src/domain/**` -- **FILE-007**: Mapper abstractions under - `packages/ddd/src/infrastructure/mapper/` and - `packages/ddd/src/domain/types/mapper.type.ts` -- **FILE-008**: `packages/ddd/README.md`, `packages/ddd/docs/DDD-GUIDE.md`, - `packages/ddd/docs/MIGRATION.md` - -## 6. Testing - -- **TEST-001**: Constructing an invalid concrete entity throws only after the - concrete constructor explicitly validates. -- **TEST-002**: A failed mutation leaves all prior entity props unchanged. -- **TEST-003**: Entity dates cannot be changed through `setTime` or returned - mutable references. -- **TEST-004**: An aggregate cannot record an event for another aggregate. -- **TEST-005**: Event collection preserves order and pull clears only returned - events. -- **TEST-006**: Invalid event IDs, names, versions, timestamps, and payloads are - rejected. -- **TEST-007**: Nested event payloads cannot be mutated after construction and - serialized output is detached. -- **TEST-008**: Creation emits creation events only when intended; mapper - rehydration emits none. -- **TEST-009**: Package type tests, unit tests, lint, and build pass; affected - authentication tests pass. Authentication typecheck still reports unrelated - pre-existing repository symbols and registry/test fixture errors. - -## 7. Risks & Assumptions - -- **RISK-001**: Existing authentication aggregates may rely on the old - base-constructor validation or public event API; all call sites must migrate - atomically. -- **RISK-002**: Replacing mutable `Date` exposure may require small changes to - domain code that currently mutates dates in place. -- **RISK-003**: Requiring explicit event IDs/timestamps may require a shared - application-level ID/clock provider; this must remain outside the domain - model. -- **ASSUMPTION-001**: Authentication package changes are in scope because they - directly consume the breaking `@rineex/ddd` API. -- **ASSUMPTION-002**: Mappers are the approved persistence rehydration boundary. -- **ASSUMPTION-003**: No event store or event-sourcing behavior is required by - this task. - -## 8. Related Specifications / Further Reading - -- [DDD primitive hardening design](../docs/superpowers/specs/2026-08-08-ddd-primitives-design.md) -- [DDD usage guide](../packages/ddd/docs/DDD-GUIDE.md) -- [DDD API migration guide](../packages/ddd/docs/MIGRATION.md) -- [Clean DDD and Hexagonal skill](../.agents/skills/clean-ddd-hexagonal/SKILL.md) From 5dade6763cb4d743a0371009b167e740a66f40d8 Mon Sep 17 00:00:00 2001 From: Hossein Pourdavar Date: Fri, 2 Oct 2026 09:27:19 +0330 Subject: [PATCH 2/3] docs: expand guides and add agent reference --- README.md | 29 ++- docs/api-reference.md | 90 +++++++++ docs/architecture.md | 80 +++++--- docs/authentication.md | 144 ++++++++++--- docs/ddd.md | 171 ++++++++++++++-- docs/decision-engine.md | 189 +++++++++++++----- docs/development.md | 85 +++++--- docs/nestjs-integrations.md | 124 +++++++++--- docs/packages.md | 43 ++-- llms.txt | 36 ++++ packages/authentication/methods/otp/README.md | 8 +- 11 files changed, 798 insertions(+), 201 deletions(-) create mode 100644 docs/api-reference.md create mode 100644 llms.txt diff --git a/README.md b/README.md index 8994bd1..0244b9a 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,24 @@ developing this repository. ## Documentation -- [Development workflow](docs/development.md) -- [Architecture](docs/architecture.md) -- [Package catalog](docs/packages.md) -- [Decision engine](docs/decision-engine.md) -- [Authentication](docs/authentication.md) -- [NestJS integrations](docs/nestjs-integrations.md) -- [DDD primitives](docs/ddd.md) - -Every package has a focused README next to its source. Package release history +- [Development workflow](docs/development.md) — prerequisites, workspace + commands, testing, releases, and package generation. +- [Architecture](docs/architecture.md) — module boundaries, dependency + direction, and public API rules. +- [Package catalog](docs/packages.md) — package names, dependencies, exports, + and supported status. +- [API reference](docs/api-reference.md) — public export inventory and + package-specific contracts. +- [Decision engine](docs/decision-engine.md) — pipeline behavior, complete + selection example, events, and extension points. +- [Authentication](docs/authentication.md) — core contracts, policy evaluation, + OTP integration, and current passwordless boundary. +- [NestJS integrations](docs/nestjs-integrations.md) — Redis, Slonik, middleware + configuration, and lifecycle behavior. +- [DDD primitives](docs/ddd.md) — value objects, entities, aggregates, events, + results, errors, and mappers. +- [LLM and agent reference](llms.txt) — a single machine-oriented entry point + that links to the authoritative human guides. + +Every package has a focused README beside its source. Package release history remains in its `CHANGELOG.md`. diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..ea9a172 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,90 @@ +# API reference + +This reference inventories the supported package-root API. Names are grouped by +responsibility so readers can navigate the source without treating internal file +paths as public contracts. + +## `@rineex/ddd` + +| Area | Public API | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Base modeling | `ValueObject`, `PrimitiveValueObject`, `Entity`, `AggregateRoot`, `DomainEvent` | +| IDs and value objects | `AggregateId`, `DomainID`, `Email`, `IPAddress`, `Timestamp`, `Url`, `UserAgent`, `UserAgentProps` | +| Errors and outcomes | `DomainError`, `Result`, `Ok`, `Err`, `UseCaseError`, core error registry/types, internal/invalid-state/invalid-value/timeout errors, entity-validation and invalid-value-object errors | +| Types | deep primitive/immutable, entity brand/ID, mapper, and value-object-like types | +| Services and mapping | application-service and logger ports, clock port, `BaseMapper` | +| Utilities | `deepFreeze`, HTTP status constants | + +`Result` is a discriminated union with `kind: 'ok' | 'err'`. Use `Result.ok`, +`Result.err`, `Result.void`, `Result.match`, `Result.map`, `Result.mapError`, +`Result.flatMap`, `Result.isOk`, `Result.isErr`, and `Result.isResult`. + +## `@rineex/decision-engine` + +| Area | Public API | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Execution | `DecisionEngine`, `DecisionValidator`, `DefaultDecisionValidator` | +| Definition and input | `DecisionDefinition`, `DecisionRequest`, `DecisionPolicy`, `DecisionContext`, `CandidateRefResolver` | +| Pipeline records | `CandidateEvaluation`, `DecisionResult`, `DecisionExecution`, `DecisionEvent` | +| Constraints/features | `Constraint`, `ConstraintResult`, `Feature`, `FeatureValue`, `FeatureObjective` | +| Strategies | `Normalizer`, `MinMaxNormalizer`, `ScoringStrategy`, `WeightedSumScoringStrategy`, `FeatureWeightResolver`, `RankingStrategy`, `DescendingScoreRankingStrategy`, `SelectionStrategy`, `SelectFirstStrategy`, `SelectTopNStrategy`, `SelectAboveThresholdStrategy` | +| Events/publication | `CandidateRejectedEvent`, `DecisionCompletedEvent`, `DecisionEventPublisher`, `DecisionEventHandler`, `InMemoryDecisionEventPublisher` | +| Errors | `DecisionError`, `DecisionExecutionError`, `InvalidDecisionDefinitionError` | + +Use only an implementation of the exported strategy contracts. The engine +validates collaborator outputs, so custom strategies must preserve the +configured candidate/feature semantics described in the +[decision guide](decision-engine.md). + +## `@rineex/auth-core` + +The root groups exports into `domain`, `ports`, and `types`. + +- **Identity domain:** identity and authentication-attempt aggregates; identity + entity; identity, auth-attempt, factor, method, policy, provider, and status + value objects; authentication started/failed/succeeded and identity + created/disabled events. +- **Policy domain:** `AuthPolicyEvaluator`, `AuthPolicyEngine`, + `AuthPolicyContext`, and `AuthPolicyDecision`. +- **Inbound port:** `AuthMethodPort` and `AuthMethodOutcome`. +- **Outbound ports:** identity, authentication-attempt, session, token, and + OAuth-authorization repositories; session, identity, and domain-event + publisher adapters; logging, MFA, and observability ports. +- **Types:** `AuthContext`, `AuthFactor`, `AuthMethod`, `AuthPolicy`, + `IdentityProvider`, `RiskSignal`, and observability event types. + +Only exports re-exported by those three root barrels are supported. In +particular, service classes and OAuth/MFA implementation files are not public +root API. + +## Authentication method packages + +`@rineex/authentication-method-otp` exports `OtpAuthMethod` and +`OtpChannelPort`. Its current constructor type depends on an internal, +non-root-exported `OtpCode`, so do not treat it as a complete constructible +public integration yet. + +`@rineex/authentication-method-passwordless` exports nothing from its root. Its +implementation is intentionally not documented as a stable API. + +## NestJS adapters + +| Package | Public API | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@rineex/ioredis` | `RedisModule`, `RedisModuleAsyncOptions`, single/cluster/module option types, `InjectRedis`, `getRedisConnectionToken`, `getRedisOptionsToken`, `createRedisConnection`, `RedisHealthModule`, `RedisHealthIndicator` | +| `@rineex/pg-slonik` | `SlonikModule`, `InjectPool`, configurable-module exports (`MODULE_OPTIONS_TOKEN`, `OPTIONS_TYPE`, `ASYNC_OPTIONS_TYPE`), `createSlonikToken` | +| Cookie parser module | `CookieParserModule`, `CookieParserMiddleware`, `CookieParseOptions` | +| CORS module | `CorsModule`, `CorsMiddleware`, its configurable-module exports | +| Helmet module | `HelmetModule`, `HelmetMiddleware` | +| Favicon-ignore module | `IgnoreFaviconModule`, `IgnoreFaviconMiddleware` | +| Response-time module | `ResponseTimeModule`, `ResponseTimeMiddleware`, `ResponseTimeOptions` | + +Nest configurable modules expose generated registration APIs. Use their +documented `register`/`registerAsync` calls rather than manually constructing +module providers. + +## Configuration and reserved packages + +`@rineex/eslint-config` publishes the `base`, `next-js`, `react-internal`, and +`db` configuration entry points. `@rineex/typescript-config` supplies +`base.json` and `react-library.json`. `@rineex/libs` currently exports no API. diff --git a/docs/architecture.md b/docs/architecture.md index 038e591..bba712c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,34 +1,68 @@ # Architecture -Rineex Core is a library monorepo, not an application. The root package -orchestrates workspace-wide build, test, lint, formatting, and release commands. -Each package owns its source, build configuration, tests, and public entry -point. +Rineex Core is a library monorepo, not a runnable application. The root package +owns repository-wide tasks. Each library owns source, tests, build +configuration, package metadata, and a single public entry point. -## Dependency direction - -`@rineex/ddd` supplies general domain abstractions. Authentication packages -build on it. The decision engine is standalone. NestJS integration packages -adapt third-party infrastructure to Nest's dependency-injection lifecycle and -should remain at the edge of an application. +## Layers and dependency direction ```text -@rineex/ddd ──> @rineex/auth-core ──> authentication method packages +@rineex/ddd + └─ generic domain modeling, errors, results, mapper support + └─ @rineex/auth-core + └─ OTP and passwordless method packages + +@rineex/decision-engine + └─ standalone deterministic decision pipeline + +Application composition root + └─ NestJS adapters + ├─ @rineex/ioredis + ├─ @rineex/pg-slonik + └─ HTTP middleware modules + └─ third-party infrastructure +``` + +The dependency direction is intentional. Core domain packages should not import +NestJS, a database driver, or a transport. Nest adapters sit at the application +boundary and manage framework life cycles. `@rineex/decision-engine` is +independent of both groups. + +## Public API contract + +The supported API of a package is exactly the set of exports from its +`src/index.ts`. Do not import a package's other source files through a deep +path; those files may move, become private, or disappear without a compatibility +guarantee. The documentation identifies packages with deliberately empty entry +points so consumers do not mistake internal source for a supported API. + +## Package output + +Publishable libraries generally declare these fields: -application ──> @rineex/decision-engine -application ──> NestJS integration packages ──> Redis / Slonik / Express middleware +```json +{ + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts" +} ``` -## Public API rule +tsup builds these outputs from `src/index.ts`. Configuration packages are +private workspace dependencies and intentionally expose configuration files +instead of runtime APIs. -Treat only a package's `src/index.ts` exports as its supported API. Source files -not exported there are implementation details, even when they are visible in the -repository. This protects consumers from deep-import breakage and keeps -documentation aligned with published artifacts. +## Choosing a package -## Build artifacts +- Model domain concepts and use explicit success/failure values with + `@rineex/ddd`. +- Select candidates by constraints and weighted preferences with + `@rineex/decision-engine`. +- Build authentication domain state/contracts with `@rineex/auth-core`; add OTP + through its dedicated method package. +- Wire Redis or PostgreSQL into a Nest application with the matching Nest + adapter. +- Apply common Express middleware globally in Nest with the middleware modules. -Packages declare `main`, `module`, and `types` paths under `dist/`; tsup -produces those artifacts. Configuration packages are private workspace -dependencies and expose configuration files rather than a runtime library entry -point. +The [package catalog](packages.md) is the complete index; detailed guides +explain each integration boundary. diff --git a/docs/authentication.md b/docs/authentication.md index 8319f92..c768a15 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -1,38 +1,134 @@ # Authentication -Authentication packages separate core domain contracts from method -implementations. `@rineex/auth-core` is the supported public foundation; adapt -its repository, session, event-publisher, log, and authentication-method ports -in the application layer. +The authentication packages separate core domain contracts from method +implementations. `@rineex/auth-core` is the stable public foundation. +Applications provide persistence, session, event-publishing, logging, and +transport adapters through its exported ports. -## Core public surface +## Install + +```bash +pnpm add @rineex/auth-core @rineex/ddd +pnpm add @rineex/authentication-method-otp +``` + +## Core public API `@rineex/auth-core` exports identity aggregates/entities/value objects and -lifecycle events, policy contracts and `AuthPolicyEngine`, inbound/outbound -ports, and authentication context/factor/method/policy/provider/risk types. It -does not export its application services, OAuth implementation, MFA -implementation, or infrastructure code from the package root. +lifecycle events; policy contracts and `AuthPolicyEngine`; inbound and outbound +ports; and auth context, factor, method, policy, provider, and risk types. Its +root entry point does **not** export application services, MFA/OAuth +implementation files, repositories, or infrastructure adapters. Treat those +source files as internal. -This means consumers should compose the public domain contracts rather than -importing internal service paths. +The public `AuthMethodPort` is the plugin boundary: + +```ts +type AuthMethodPort = { + readonly method: AuthMethodName; + start(params: { + authAttemptId: AuthAttemptId; + ctx: unknown; + }): AuthMethodOutcome | Promise; + verify(params: { + authAttemptId: AuthAttemptId; + payload: unknown; + }): AuthMethodOutcome | Promise; +}; +``` -## OTP method +An outcome is `{ ok: true }` or `{ ok: false, violation: UseCaseError }`. An +authentication method should validate the transport-shaped `ctx`/`payload`, +translate it to domain values, and return a violation for expected invalid input +rather than leaking transport concerns into the core domain. -`OtpAuthMethod` implements the core authentication-method port. Give it an -`OtpChannelPort` and an OTP generator. The channel is responsible for delivery -and verification; it can use SMS, email, push, voice, or another transport. +## Implement persistence and event ports + +The core exports ports for identity, authentication-attempt, session, and +OAuth-authorization persistence, session/token lookup, event publication, +logging, MFA time/ID/repository support, and observability events. Implement +them in the application infrastructure layer. ```ts -const method = new OtpAuthMethod(channel, () => OtpCode.create('123456')); +import type { IdentityRepository } from '@rineex/auth-core'; + +class PostgresIdentityRepository implements IdentityRepository { + async get(id: IdentityId): Promise { + // Load persistence data and map it to Identity. + return null; + } + + async save(identity: Identity): Promise { + // Persist identity state and its pulled domain events atomically. + } +} ``` -The `start` input must expose an `IdentityId` at `ctx.identityId` or -`ctx.metadata.identityId`. The `verify` payload must contain an `IdentityId` and -a string `code`. +Repositories operate on domain types, never on request/response DTOs. Publish +aggregate events through `DomainEventPublisherPort` after the persistence +transaction succeeds. + +## Authentication policy + +`AuthPolicyEvaluator` evaluates immutable facts in an `AuthPolicyContext`. +Return `null` for no opinion, a deny decision for a hard block, or an allow +decision that may require step-up authentication. `AuthPolicyEngine` evaluates +policies in order; the first hard deny wins and any allow decision can set +`requiresStepUp`. + +```ts +import { + AuthPolicyEngine, + AuthPolicyEvaluator, + type AuthPolicyContext, +} from '@rineex/auth-core'; + +class RiskPolicy extends AuthPolicyEvaluator { + evaluate(context: AuthPolicyContext) { + if (context.isBlocked) + return { allowed: false as const, reason: 'IDENTITY_BLOCKED' }; + if ((context.riskScore ?? 0) >= 70) + return { allowed: true as const, requiresStepUp: true }; + return null; + } +} + +const decision = new AuthPolicyEngine([new RiskPolicy()]).evaluate({ + method: AuthMethod.create('otp'), + riskScore: 80, +}); +``` + +Policies are pure decision logic. Risk scoring, device reputation, IP +intelligence, and other external facts must be calculated upstream and supplied +in the context. + +## OTP method status and contract + +`OtpAuthMethod` and `OtpChannelPort` are exported from +`@rineex/authentication-method-otp`. The port sends and verifies an internal +`OtpCode` for an `IdentityId`; `start` reads an identity from `ctx.identityId` +or `ctx.metadata.identityId`, while `verify` requires +`{ identityId: IdentityId, code: string }`. Invalid context, malformed payloads, +invalid code values, and failed channel verification return +`{ ok: false, violation }`. + +> [!WARNING] The exported `OtpAuthMethod` constructor requires a generator +> returning `OtpCode`, but `OtpCode` itself is not exported from the package +> root. A consumer cannot construct that generator solely through the supported +> public API. Avoid deep imports: this package needs a public `OtpCode` export +> or a public generator abstraction before it is a complete supported +> integration surface. + +When that boundary is made public, the channel should own transmission, storage, +retry policy, rate limiting, and provider failures; the method should own only +authentication-method orchestration. -## Passwordless method status +## Passwordless package status -The passwordless package contains challenge aggregate, service, registry, port, -and channel source code, but its package entry point currently exports nothing. -It is therefore not a supported consumable API yet. Avoid deep imports until its -`src/index.ts` intentionally exports a public contract. +`@rineex/authentication-method-passwordless` contains passwordless challenge +aggregates, value objects, services, channel registries, repository/channel/ID +ports, and email/SMS channel implementations. Its current package root imports +only a declaration file and exports no runtime or type API. Do not deep-import +those files from an application; there is no compatibility contract until a +deliberate public entry point is added. diff --git a/docs/ddd.md b/docs/ddd.md index 2613492..a1b2abd 100644 --- a/docs/ddd.md +++ b/docs/ddd.md @@ -1,16 +1,159 @@ # DDD primitives -`@rineex/ddd` supplies reusable TypeScript building blocks rather than a -persistence or application framework. - -| Group | Exports | -| -------------------------- | -------------------------------------------------------------------------------------------- | -| Domain modeling | `ValueObject`, `PrimitiveValueObject`, `Entity`, `AggregateRoot`, `DomainEvent` | -| Value objects | `AggregateId`, `DomainID`, `Email`, `IPAddress`, `Timestamp`, `Url`, `UserAgent` | -| Result and errors | `Result`, `DomainError`, error registry, internal/invalid-state/invalid-value/timeout errors | -| Types and utilities | entity and mapper types, deep-primitive type, `deepFreeze` | -| Application/infrastructure | application-service and logger ports, `BaseMapper`, HTTP status constants | - -Value objects are the right boundary for validation and equality. Entities and -aggregates model identity-bearing state. Use `Result` when callers should handle -success or a typed error explicitly instead of relying on exceptions. +`@rineex/ddd` provides framework-independent building blocks for modeling a +domain. It does not prescribe a database, HTTP framework, message bus, or +dependency-injection container. + +## Install + +```bash +pnpm add @rineex/ddd +``` + +## Value objects + +`PrimitiveValueObject` represents one validated primitive. `ValueObject` +represents validated structured data, deep-freezes its props, compares by +concrete class and value, and serializes through `toJSON()`. Both expose +`value`, `equals`, `toJSON`, and `toString`. + +```ts +import { Email, PrimitiveValueObject } from '@rineex/ddd'; + +class OrderNumber extends PrimitiveValueObject { + static from(value: string) { + return new OrderNumber(value); + } + + protected validate(value: string): void { + if (!/^ORD-\d+$/.test(value)) throw new Error('Invalid order number'); + } +} + +const email = Email.fromString('Person@Example.com'); +console.log(email.value); // person@example.com +console.log(OrderNumber.from('ORD-42').equals(OrderNumber.from('ORD-42'))); // true +``` + +Built-in value objects are `AggregateId`, `DomainID`, `Email`, `IPAddress`, +`Timestamp`, `Url`, and `UserAgent`. + +- `DomainID.generate()` produces a UUID v7-backed ID; `fromString()` validates + an existing ID. +- `AggregateId` is the general aggregate identifier. +- `Email.fromString()` lowercases and validates email input. +- `IPAddress.create()` accepts valid IPv4 or IPv6 input. +- `Timestamp` accepts non-negative integer Unix milliseconds and has `toDate()`. +- `Url.create()` validates a URL and exposes `href`. +- `UserAgent` accepts a raw string and parsed metadata; its metadata includes + bot/mobile information. + +Invalid built-in values throw a typed domain validation error. Validate raw +input at the boundary, then pass value objects through the domain instead of raw +strings. + +## Entities and aggregates + +`Entity` compares identity, not properties. Its protected `props` are +deeply immutable. Subclasses implement `validateProps` and `toObject`; use +protected `mutate` for an invariant-checked state transition. + +```ts +import { AggregateId, Entity, type Immutable } from '@rineex/ddd'; + +type CustomerProps = { name: string; active: boolean }; + +class Customer extends Entity { + static create(name: string) { + return new Customer({ + id: AggregateId.generate(), + props: { name, active: true }, + }); + } + + deactivate() { + this.mutate(current => ({ ...current, active: false })); + } + + protected validateProps(props: Immutable) { + if (!props.name.trim()) throw new Error('Customer name is required'); + } + + toObject() { + return { + id: this.id.value, + name: this.props.name, + active: this.props.active, + }; + } +} +``` + +`AggregateRoot` extends `Entity` and records domain events. `recordEvent` +requires the event aggregate ID to equal the root's ID. `domainEvents` returns a +read-only copy; `pullDomainEvents()` returns and clears the pending event +buffer. Persist the aggregate and publish its pulled events through an +application-owned transaction boundary. + +## Domain events + +Extend `DomainEvent` for facts that occurred in the domain. Its constructor +validates ID, event name, schema version, timestamp, and a JSON-safe payload; +payloads are deeply frozen. `toPrimitives()` returns a transport-safe record. + +```ts +import { AggregateId, DomainEvent } from '@rineex/ddd'; + +class CustomerCreated extends DomainEvent {} + +const event = new CustomerCreated({ + id: crypto.randomUUID(), + eventName: 'customer.created', + aggregateId: AggregateId.generate(), + schemaVersion: 1, + occurredAt: Date.now(), + payload: { email: 'person@example.com' }, +}); +``` + +## Results and errors + +Use exceptions for broken domain invariants and `Result` for expected +application outcomes. + +```ts +import { + Result, + type Result as ResultType, + type UseCaseError, +} from '@rineex/ddd'; + +type RegisterResult = ResultType<{ id: string }, UseCaseError>; + +function respond(result: RegisterResult) { + return Result.match(result, { + ok: value => ({ status: 201, body: value }), + err: error => ({ status: 400, body: error.toObject?.() ?? error }), + }); +} +``` + +`Result.ok`, `Result.err`, `Result.void`, `isOk`, `isErr`, `isResult`, `map`, +`mapError`, `flatMap`, and `match` are available. `AsyncResult` is +`Promise>`. + +Extend `DomainError` for a typed, machine-readable error. Its `code` follows +`NAMESPACE.ERROR_NAME`, metadata is immutable and limited to primitives, and +`toObject()` returns code/message/metadata. Use `InferErrorCodes` and an error +registry to derive valid codes. Core errors include internal, invalid-state, +invalid-value, and timeout cases. + +## Application and infrastructure helpers + +The package exports application-service and logger ports, a clock port, HTTP +status constants, deep immutable/entity/mapper types, and `deepFreeze`. + +`BaseMapper` provides `toDomainList`, `toDTOList`, and `toPersistenceList` +around the abstract `toDomain`, `toDomainProps`, `toDTO`, and `toPersistence` +operations. Keep database models and framework DTOs outside domain entities; map +at the infrastructure boundary. diff --git a/docs/decision-engine.md b/docs/decision-engine.md index 68a87bd..306c968 100644 --- a/docs/decision-engine.md +++ b/docs/decision-engine.md @@ -1,63 +1,83 @@ # Decision engine -`@rineex/decision-engine` is a deterministic, synchronous pipeline for choosing -candidates. It has no persistence, I/O, logging, or implicit event dispatch; -callers own those concerns. +`@rineex/decision-engine` is a framework-independent, deterministic, synchronous +decision pipeline. Give it a reusable definition and one runtime request; it +returns an immutable decision result and the events that describe the completed +decision. It never performs I/O, persistence, logging, telemetry, or event +publication. -## Execution model +## Install -1. Validate the definition and request. -2. Evaluate every hard constraint. -3. Extract features for eligible candidates. -4. Normalize feature values, score candidates, and rank them. -5. Select the final ranked candidates. -6. Return frozen evaluations, a result, and `candidate.rejected` / - `decision.completed` events. +```bash +pnpm add @rineex/decision-engine +``` + +## Mental model -Constraints explain ineligibility. Features are numeric preferences with -`FeatureObjective.MAXIMIZE` or `FeatureObjective.MINIMIZE`. A policy provides -the weights or other business values used by strategies. +A definition is the reusable algorithm. A request contains a specific candidate +set, context, policy, and optional correlation ID. The policy is versioned so +completed decisions can be audited and reproduced. + +```text +definition + request + → validate + → constraints (all candidates) + → features (eligible candidates only) + → normalize + → score + → rank + → select + → immutable result + domain events +``` -## Example +Constraints are hard eligibility rules. A failed constraint does not stop +evaluation of the remaining constraints: the output preserves every failure for +explainability. Features are numeric preferences. Their objective declares +whether larger (`MAXIMIZE`) or smaller (`MINIMIZE`) raw values are better. + +## Complete example ```ts import { DecisionEngine, DefaultDecisionValidator, DescendingScoreRankingStrategy, + type DecisionDefinition, type DecisionPolicy, FeatureObjective, + InMemoryDecisionEventPublisher, MinMaxNormalizer, SelectTopNStrategy, WeightedSumScoringStrategy, } from '@rineex/decision-engine'; type Supplier = { - id: string; - cost: number; - quality: number; - approved: boolean; + readonly id: string; + readonly cost: number; + readonly quality: number; + readonly approved: boolean; }; -type Policy = DecisionPolicy & { - values: { - qualityWeight: number; - costWeight: number; + +interface SupplierPolicy extends DecisionPolicy { + readonly values: { + readonly qualityWeight: number; + readonly costWeight: number; }; -}; +} -const definition = { +const definition: DecisionDefinition = { id: 'supplier-selection', version: 'v1', - candidateRefResolver: { resolve: (supplier: Supplier) => supplier.id }, + candidateRefResolver: { resolve: supplier => supplier.id }, constraints: [ { id: 'approved', - evaluate: (supplier: Supplier) => + evaluate: supplier => supplier.approved - ? { constraintId: 'approved', satisfied: true as const } + ? { constraintId: 'approved', satisfied: true } : { constraintId: 'approved', - satisfied: false as const, + satisfied: false, reasonCode: 'NOT_APPROVED', }, }, @@ -66,7 +86,7 @@ const definition = { { key: 'quality', objective: FeatureObjective.MAXIMIZE, - evaluate: (supplier: Supplier) => ({ + evaluate: supplier => ({ key: 'quality', objective: FeatureObjective.MAXIMIZE, rawValue: supplier.quality, @@ -75,7 +95,7 @@ const definition = { { key: 'cost', objective: FeatureObjective.MINIMIZE, - evaluate: (supplier: Supplier) => ({ + evaluate: supplier => ({ key: 'cost', objective: FeatureObjective.MINIMIZE, rawValue: supplier.cost, @@ -83,7 +103,7 @@ const definition = { }, ], normalizer: new MinMaxNormalizer(), - scoringStrategy: new WeightedSumScoringStrategy({ + scoringStrategy: new WeightedSumScoringStrategy({ resolve: (key, policy) => key === 'quality' ? policy.values.qualityWeight @@ -93,25 +113,96 @@ const definition = { selectionStrategy: new SelectTopNStrategy(1), }; -const execution = new DecisionEngine(new DefaultDecisionValidator()).execute( - definition, - { - candidates: [ - { id: 'low-cost', quality: 60, cost: 10, approved: true }, - { id: 'high-quality', quality: 100, cost: 90, approved: true }, - ], - context: undefined, - policy: { - id: 'default', - version: 'v1', - values: { qualityWeight: 0.75, costWeight: 0.25 }, - }, +const engine = new DecisionEngine(new DefaultDecisionValidator()); +const execution = engine.execute(definition, { + candidates: [ + { id: 'low-cost', quality: 60, cost: 10, approved: true }, + { id: 'high-quality', quality: 100, cost: 90, approved: true }, + { id: 'unapproved', quality: 99, cost: 20, approved: false }, + ], + context: undefined, + policy: { + id: 'default', + version: 'v1', + values: { qualityWeight: 0.75, costWeight: 0.25 }, }, -); + correlationId: 'supplier-request-42', +}); console.log(execution.result.selected); +console.log(execution.events); + +const publisher = new InMemoryDecisionEventPublisher(); +publisher.subscribe('decision.completed', { + handle: async event => console.log(event.payload.selectedCandidateRefs), +}); +await publisher.publish(execution.events); ``` -Use `InMemoryDecisionEventPublisher` only when an in-memory ordered dispatcher -fits the application. The engine itself merely returns events; publish them -after a successful execution boundary. +## Definition contract + +| Field | Requirement | +| ---------------------- | ---------------------------------------------------------------------------- | +| `id`, `version` | non-blank stable identifiers | +| `candidateRefResolver` | resolves a non-blank, unique reference for each candidate | +| `constraints` | deterministic eligibility rules; results must use the declared constraint ID | +| `features` | deterministic numeric measurements; feature keys must be stable and unique | +| `normalizer` | preserves the configured feature set and produces valid normalized values | +| `scoringStrategy` | returns a finite score | +| `rankingStrategy` | produces a valid ranking over eligible candidates | +| `selectionStrategy` | returns only candidates from the ranked eligible set | + +`DefaultDecisionValidator` checks the definition and request before processing. +The engine then validates runtime outputs from every collaborator. Invalid +candidate references, non-finite raw values or scores, feature-set changes, and +selections outside the ranked set cause an `InvalidDecisionDefinitionError` or +`DecisionExecutionError`. + +## Built-in strategies + +`MinMaxNormalizer` maps feature values to a comparable range and honors their +objective. When every raw value for a feature is equal, it normalizes that +feature to `1` for all eligible candidates. + +`WeightedSumScoringStrategy` asks a `FeatureWeightResolver` for each feature +weight and sums normalized value × weight. Weights must be finite. +`DescendingScoreRankingStrategy` ranks highest scores first and preserves input +order for ties. + +Choose selection by outcome: + +```ts +new SelectFirstStrategy(); +new SelectTopNStrategy(3); // positive safe integer only +new SelectAboveThresholdStrategy(0.8); // finite threshold only +``` + +All built-in selectors require already ranked, eligible evaluations. +`SelectAboveThresholdStrategy` requires finite candidate scores. + +## Result and events + +`execution.result` includes definition and policy IDs/versions, optional +correlation ID, every evaluation, and the selected subset ordered by rank. Each +`CandidateEvaluation` contains constraint results, eligibility, feature values, +optional score/rank, and `selected` status. + +The engine emits, in order: + +1. One `candidate.rejected` event per ineligible candidate, including its + reference and failed constraints. +2. One `decision.completed` event, including candidate, eligible, and rejected + counts plus selected references. + +The result, evaluations, features, events, and execution container are frozen +before return. Treat the output as a decision record, not a mutable working +object. Publish events only after the appropriate application transaction or +persistence boundary succeeds. + +## Extension points + +Implement `Constraint`, `Feature`, `Normalizer`, `ScoringStrategy`, +`RankingStrategy`, or `SelectionStrategy` for custom behavior. Keep +implementations deterministic and side-effect free. Put HTTP calls, database +access, random input, time acquisition, and message publication outside the +engine; pass those facts into the typed request context or policy first. diff --git a/docs/development.md b/docs/development.md index 2eac1b9..d78c82d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,54 +1,81 @@ # Development workflow -This workspace uses pnpm workspaces and Turborepo. Install dependencies with the -pnpm version declared by the root `package.json`. +This repository is a pnpm workspace orchestrated by Turborepo. The root +`package.json` pins pnpm 10.22.0 and sets Node 24.12.0 through Volta. Use that +toolchain for development; `@rineex/pg-slonik` itself supports Node 18 or later. -## Commands +## First checkout ```bash +corepack enable pnpm install pnpm build pnpm test -pnpm test:types -pnpm check-types -pnpm lint -pnpm lint:fix -pnpm prettier:write ``` -Turborepo runs package tasks with their declared dependency order. `test` -depends on builds; `test:types` runs only packages that define type tests. +`pnpm build` runs every package build in dependency order. `pnpm test` first +builds required packages, then runs each package test task. A clean test run can +emit expected test-double logs from Redis and Slonik tests; the command result +is authoritative. -Run a single package task with its package name: +## Workspace commands + +| Command | What it runs | +| --------------------- | -------------------------------------------- | +| `pnpm build` | `turbo run build` for all workspace packages | +| `pnpm test` | `turbo run test`, after required builds | +| `pnpm test:types` | declared `tsd` type tests | +| `pnpm check-types` | TypeScript no-emit checks | +| `pnpm lint` | package lint tasks | +| `pnpm lint:fix` | package lint fixes where implemented | +| `pnpm prettier` | checks formatting without writing | +| `pnpm prettier:write` | writes formatting changes | + +Run an individual package task with a package filter: ```bash -pnpm --filter @rineex/decision-engine test -pnpm --filter @rineex/ddd check-types +pnpm --filter @rineex/ddd test +pnpm --filter @rineex/decision-engine check-types pnpm --filter @rineex/pg-slonik build ``` -## Build and test conventions - -Most TypeScript packages use tsup and Vitest. The cookie-parser, CORS, -favicon-ignore, and response-time Nest modules use Jest; the Helmet module uses -Vitest. Source tests live alongside source files or in a package `tests` -directory. +## Test and build layout -## Release workflow +Most libraries use tsup for CJS, ESM, and declaration output, plus Vitest for +tests. The cookie-parser, CORS, favicon-ignore, and response-time middleware +modules declare Jest test commands. Tests may live next to implementation code +(`*.spec.ts`) or in a package-level `tests` directory. -Changesets is configured at the workspace root. Create a release entry with -`pnpm changeset`, apply selected versions with `pnpm changeset:version`, then -publish with `pnpm changeset:publish`. Build before publishing. +Do not commit generated `dist` output unless a package explicitly tracks it. The +root pre-commit hook formats and spell-checks staged Markdown, JSON, YAML, HTML, +TypeScript, and TSX files. The pre-push hook runs `pnpm test`. -## Create a package +## Add or change a package -The generator creates a new `packages/` library skeleton, including -TypeScript, tsup, Vitest, ESLint, and package metadata. +The package generator creates a standard library skeleton: ```bash pnpm g:pkg my-package "Short package description" ``` -Names must use lowercase letters, numbers, and hyphens. The generator does not -add exports or business code; add public exports to the generated `src/index.ts` -before publishing. +The name accepts only lowercase letters, numbers, and hyphens. The generator +creates package metadata, TypeScript and tsup configs, Vitest config, ESLint +config, and an empty `src/index.ts`. It does not decide the public API: add +deliberate exports to `src/index.ts`, write tests, and document the package +README before publishing. + +## Publishing + +This repository uses Changesets. + +```bash +pnpm changeset +pnpm changeset:version +pnpm build +pnpm changeset:publish +``` + +Create a changeset for every consumer-visible package change. +`changeset:version` also formats the repository. Published packages expose +artifacts from `dist`; validate the package entry point rather than relying on +internal source paths. diff --git a/docs/nestjs-integrations.md b/docs/nestjs-integrations.md index 1ecbf7f..dcc88b0 100644 --- a/docs/nestjs-integrations.md +++ b/docs/nestjs-integrations.md @@ -1,53 +1,119 @@ # NestJS integrations -These packages use Nest configurable modules and apply their middleware to all -routes. Install the package together with the peer dependencies shown in its -`package.json`. +These packages adapt third-party infrastructure to NestJS dependency injection +and lifecycle hooks. Install a module together with the peer dependencies +declared in its package metadata. The HTTP middleware packages configure all +routes with `forRoutes('*')`. ## Redis -`RedisModule.register(options, { connection })` configures a single Redis -URL/options object or a cluster-node list. Inject the created client with -`@InjectRedis()` or `@InjectRedis('name')`. `RedisHealthModule` exposes -`RedisHealthIndicator.isHealthy(key)` for Terminus health checks. +```bash +pnpm add @rineex/ioredis ioredis @nestjs/common @nestjs/terminus +``` + +Register a single connection with a URL or ioredis options: + +```ts +import { Module } from '@nestjs/common'; +import { InjectRedis, RedisModule } from '@rineex/ioredis'; +import type Redis from 'ioredis'; + +@Module({ + imports: [ + RedisModule.register( + { type: 'single', url: process.env.REDIS_URL }, + { connection: 'cache' }, + ), + ], +}) +export class AppModule {} + +class CacheService { + constructor(@InjectRedis('cache') private readonly redis: Redis) {} +} +``` -## PostgreSQL with Slonik +For a cluster, use `{ type: 'cluster', nodes, options }`. +`getRedisConnectionToken(connection?)` and `getRedisOptionsToken(connection?)` +expose the generated injection-token strings. `RedisModule` is global, tracks +created clients, pings them during application bootstrap, and attempts `quit()` +for every tracked client on shutdown. -Register each named pool and inject it by name: +`RedisHealthModule` exports `RedisHealthIndicator`. Its `isHealthy(key)` pings +the configured health client and returns the Terminus `up` or `down` result. The +health client is supplied through the `REDIS_HEALTH_INDICATOR` provider token. + +## PostgreSQL through Slonik + +```bash +pnpm add @rineex/pg-slonik slonik @nestjs/common +``` ```ts +import { Module } from '@nestjs/common'; +import { InjectPool, SlonikModule } from '@rineex/pg-slonik'; +import type { DatabasePool } from 'slonik'; + @Module({ imports: [ SlonikModule.register({ - connections: [{ name: 'DEFAULT', dsn: process.env.DATABASE_URL! }], + connections: [ + { name: 'DEFAULT', dsn: process.env.DATABASE_URL! }, + { name: 'replica', dsn: process.env.REPLICA_DATABASE_URL! }, + ], }), ], }) export class AppModule {} -class Repository { - constructor(@InjectPool() private readonly pool: DatabasePool) {} +class UserRepository { + constructor( + @InjectPool() private readonly writer: DatabasePool, + @InjectPool('replica') private readonly reader: DatabasePool, + ) {} } ``` -Pools are verified during creation, retried on connection failure (nine retries -with a three-second delay by default), and closed during application shutdown. -`SlonikModule` is global by default. +Every configured connection has `name`, `dsn`, optional Slonik `options`, and +optional `tags`. `InjectPool(name?)` injects the matching pool; +`createSlonikToken(name?)` produces its token. The module is global by default; +set the configurable-module extra `isGlobal: false` when module-local scope is +required. + +`createSlonikConnection` creates a pool, verifies it through `pool.connect`, and +retries failures. `handleRetry` defaults to nine retries and a three-second +delay; it accepts alternative attempt count, delay, pool name, verbose logging, +and retry predicate. `SlonikModule` ends registered pools during shutdown. The +current public `SlonikModule.register` override accepts only the connection +options, so its configurable-module `isGlobal` extra remains at its default +`true` through that public method. + +## Global HTTP middleware -## HTTP middleware modules +Each middleware module supports Nest's generated `register` and `registerAsync` +methods. Import one module once at an appropriate application level. -All modules use `register` or `registerAsync` from Nest's -`ConfigurableModuleBuilder` and attach middleware to `'*'` routes. +```ts +@Module({ + imports: [ + CookieParserModule.register({ secret: process.env.COOKIE_SECRET }), + CorsModule.register({ origin: ['https://app.example.com'] }), + HelmetModule.register(), + IgnoreFaviconModule.register(), + ResponseTimeModule.register({ header: 'X-Response-Time', digits: 1 }), + ], +}) +export class HttpModule {} +``` -| Module | Export | Underlying package | -| -------------- | --------------------- | ------------------ | -| Cookie parser | `CookieParserModule` | `cookie-parser` | -| CORS | `CorsModule` | `cors` | -| Helmet | `HelmetModule` | `helmet` | -| Ignore favicon | `IgnoreFaviconModule` | built in | -| Response time | `ResponseTimeModule` | `response-time` | +| Package/module | Options and behavior | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CookieParserModule` | Passes `CookieParseOptions` to `cookie-parser` for every request. | +| `CorsModule` | Passes `CorsOptions` to `cors`. Defaults: credentials enabled, module allowed methods/headers, `204` preflight status; supplied options override defaults. | +| `HelmetModule` | Passes `HelmetOptions` to `helmet`. | +| `IgnoreFaviconModule` | No meaningful options; returns `204` if `req.originalUrl` contains `favicon.ico`, otherwise calls next. | +| `ResponseTimeModule` | Passes `digits`, `header`, and `suffix` options to `response-time`. | -`CorsModule` defaults to credentials enabled, standard allowed methods/headers, -and `204` for preflight responses; supplied options override those defaults. -`IgnoreFaviconModule` responds with `204` when a request URL includes -`favicon.ico`. Response-time options are `digits`, `header`, and `suffix`. +These modules are Express-oriented: their middleware types use Express +request/response objects, so install the listed Express peer dependency and use +them with a compatible Nest HTTP adapter. diff --git a/docs/packages.md b/docs/packages.md index af0d9ee..e6eb444 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -1,23 +1,26 @@ # Package catalog -| Package | Purpose | Package guide | -| -------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- | -| `@rineex/ddd` | Domain primitives, errors, results, and value objects | [README](../packages/ddd/README.md) | -| `@rineex/decision-engine` | Deterministic candidate evaluation and selection | [README](../packages/decision-engine/README.md) | -| `@rineex/auth-core` | Authentication domain contracts and types | [README](../packages/authentication/core/README.md) | -| `@rineex/authentication-method-otp` | OTP auth-method implementation | [README](../packages/authentication/methods/otp/README.md) | -| `@rineex/authentication-method-passwordless` | Internal passwordless challenge implementation | [README](../packages/authentication/methods/passwordless/README.md) | -| `@rineex/ioredis` | NestJS Redis connection and health support | [README](../packages/ioredis/README.md) | -| `@rineex/pg-slonik` | NestJS Slonik pool registration and injection | [README](../packages/pg-slonik/README.md) | -| `@rineex/cookie-parser-mw-module` | Cookie-parser middleware module | [README](../packages/nest/cookie-middleware-module/README.md) | -| `@rineex/cors-mw-module` | CORS middleware module | [README](../packages/nest/cors-middleware-module/README.md) | -| `@rineex/helmet-mw-module` | Helmet middleware module | [README](../packages/nest/helmet-middleware-module/README.md) | -| `@rineex/favicon-ignore-mw-module` | Favicon suppression middleware module | [README](../packages/nest/no-favicon-middleware-module/README.md) | -| `@rineex/response-time-mw-module` | Response-time middleware module | [README](../packages/nest/response-time-middleware-module/README.md) | -| `@rineex/libs` | Placeholder package with no public exports | [README](../packages/libs/README.md) | -| `@rineex/eslint-config` | Private shared ESLint flat configurations | [README](../packages/eslint-config/README.md) | -| `@rineex/typescript-config` | Private shared TypeScript configurations | [README](../packages/typescript-config/README.md) | +The table lists every workspace package and its consumer-facing status. “Public +exports” refers only to the package root entry point. -The two configuration packages are private workspace dependencies. They are -consumed by repository packages, not distributed as runtime application -dependencies. +| Package | Purpose | Runtime dependencies | Public exports | Guide | +| -------------------------------------------- | --------------------------------------- | -------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------- | +| `@rineex/ddd` | Domain primitives | `zod`, `uuid`, `rxjs`, utilities | Domain model, `Result`, errors, value objects, mapper | [README](../packages/ddd/README.md) | +| `@rineex/decision-engine` | Candidate selection pipeline | none | Engine, models, strategies, events, in-memory publisher | [README](../packages/decision-engine/README.md) | +| `@rineex/auth-core` | Authentication domain contracts | `@rineex/ddd` | domain aggregates/types, policy engine, ports | [README](../packages/authentication/core/README.md) | +| `@rineex/authentication-method-otp` | OTP auth method | auth core, DDD | `OtpAuthMethod`, `OtpChannelPort` | [README](../packages/authentication/methods/otp/README.md) | +| `@rineex/authentication-method-passwordless` | Passwordless implementation in progress | auth core, DDD | none | [README](../packages/authentication/methods/passwordless/README.md) | +| `@rineex/ioredis` | Nest Redis adapter | Nest, ioredis, Terminus | module, decorator, options, helpers, health indicator | [README](../packages/ioredis/README.md) | +| `@rineex/pg-slonik` | Nest Slonik adapter | Nest, Slonik, RxJS | module, injection decorator, module-definition types, token helper | [README](../packages/pg-slonik/README.md) | +| `@rineex/cookie-parser-mw-module` | Global cookie middleware | Nest, Express, cookie-parser | module, middleware, `CookieParseOptions` | [README](../packages/nest/cookie-middleware-module/README.md) | +| `@rineex/cors-mw-module` | Global CORS middleware | Nest, Express, cors | module, middleware | [README](../packages/nest/cors-middleware-module/README.md) | +| `@rineex/helmet-mw-module` | Global Helmet middleware | Nest, Express, helmet | module, middleware | [README](../packages/nest/helmet-middleware-module/README.md) | +| `@rineex/favicon-ignore-mw-module` | Ignore favicon requests | Nest, Express | module, middleware | [README](../packages/nest/no-favicon-middleware-module/README.md) | +| `@rineex/response-time-mw-module` | Global response-time middleware | Nest, Express, response-time | module, middleware, options type | [README](../packages/nest/response-time-middleware-module/README.md) | +| `@rineex/libs` | Reserved shared-library package | none | none | [README](../packages/libs/README.md) | +| `@rineex/eslint-config` | Private ESLint configs | ESLint plugins | configuration entry points | [README](../packages/eslint-config/README.md) | +| `@rineex/typescript-config` | Private TypeScript configs | none | JSON configuration files | [README](../packages/typescript-config/README.md) | + +The [authentication](authentication.md), [decision engine](decision-engine.md), +[NestJS integrations](nestjs-integrations.md), and [DDD](ddd.md) guides contain +usage examples and behavior details. diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..23adfa4 --- /dev/null +++ b/llms.txt @@ -0,0 +1,36 @@ +# Rineex Core agent reference + +This is the single machine-oriented entry point for coding agents and LLMs. Read it before changing this repository, then follow the linked human documentation for the relevant domain. The source code and package `src/index.ts` files are authoritative when documentation and code disagree. + +## Repository + +- Package manager: pnpm 10.22.0. Runtime used by the workspace: Node 24.12.0. +- Workspace commands: `pnpm build`, `pnpm test`, `pnpm test:types`, `pnpm check-types`, `pnpm lint`, `pnpm prettier`. +- Target one package with `pnpm --filter