npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

consent-core

v1.0.11

Published

Lightweight, type-safe consent management library for NestJS applications with TypeORM - GDPR compliant

Readme

consent-core

Lightweight, type-safe consent management library for NestJS applications

npm version License: MIT TypeScript

A production-ready, GDPR/CCPA-compliant consent management solution for NestJS with TypeORM. Simple to integrate, highly customizable, and built with TypeScript for complete type safety.


🚀 Features

  • Type-Safe - Full TypeScript support with comprehensive type definitions
  • Framework Agnostic Auth - Works with any authentication strategy (JWT, Passport, etc.)
  • Optional Controller - Use default REST endpoints or build your own
  • Custom Entities - Extend or replace default entities with your own
  • Expiry Management - Automatic consent expiration based on validity period
  • Version Tracking - Handle policy version changes with automatic re-consent
  • Suppression Support - "Ask me later" functionality
  • GDPR/CCPA Ready - Built-in audit trails and compliance features
  • Production Ready - Used in production environments

📦 Installation

npm install consent-core

Peer Dependencies

npm install @nestjs/common @nestjs/core @nestjs/typeorm typeorm class-validator class-transformer reflect-metadata

⚡ Quick Start

1. Install and Setup Database

# Install the package
npm install consent-core

# Run the migration
psql your_database < node_modules/consent-core/migrations.sql

2. Import the Module

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConsentModule } from 'consent-core';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'your_user',
      password: 'your_password',
      database: 'your_db',
      autoLoadEntities: true,  // 👈 REQUIRED: Auto-register entities
      synchronize: false,       // Use migrations in production
    }),
    ConsentModule.forRoot(), // 👈 Add this
  ],
})
export class AppModule {}

Important: Either use autoLoadEntities: true OR explicitly register the entities:

import { ConsentDefinition, UserConsent } from 'consent-core';

TypeOrmModule.forRoot({
  // ... other config
  entities: [ConsentDefinition, UserConsent, /* your other entities */],
})

3. Secure Your Endpoints (If Using Default Controller)

The default controller expects req.user.id to be set by YOUR authentication.

// Apply your existing auth guard globally or to specific routes
@UseGuards(YourAuthGuard)  // Your existing JWT/Passport guard

The library is authentication-agnostic - use whatever you already have (JWT, Passport, sessions, etc).

4. Use the API

# Get user consent status
GET /consents/status
Authorization: Bearer <token>

# Accept a consent
POST /consents/respond
{
  "consentDefinitionId": 1,
  "status": "ACCEPTED"
}

# Suppress a consent for 7 days
POST /consents/suppress
{
  "consentDefinitionId": 1,
  "suppressDays": 7
}

📖 Usage Scenarios

Scenario 1: Default Setup (Zero Configuration) ⚡

Use Case: Quick setup with default controller and entities - perfect for getting started.

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConsentModule } from 'consent-core';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      // ... your database config
      autoLoadEntities: true,
    }),
    ConsentModule.forRoot(), // 👈 No configuration needed!
  ],
})
export class AppModule {}

What you get:

  • ✅ Default REST API endpoints (/consents/status, /consents/respond, /consents/suppress)
  • ✅ Built-in entities (ConsentDefinition, UserConsent)
  • ✅ Full service and repository layer
  • ✅ Ready to use - just run migrations and go!

Default API Endpoints:

GET  /consents/status                 # Get user's consent status
POST /consents/respond                # Accept/reject consent
POST /consents/suppress               # Suppress consent ("ask later")

Scenario 2: Custom Entities Only 🔧

Use Case: You need additional fields in consent tables but want to keep the default controller.

// entities/custom-consent-definition.entity.ts
import { Entity, Column } from 'typeorm';
import { ConsentDefinition } from 'consent-core';

@Entity('consent_definitions')
export class CustomConsentDefinition extends ConsentDefinition {
  @Column({ nullable: true })
  category: string; // Your custom field
  
  @Column({ nullable: true })
  region: string; // Your custom field
  
  @Column({ type: 'jsonb', nullable: true })
  metadata: any; // Additional data
}
// entities/custom-user-consent.entity.ts
import { Entity, Column } from 'typeorm';
import { UserConsent } from 'consent-core';

@Entity('user_consents')
export class CustomUserConsent extends UserConsent {
  @Column({ nullable: true })
  ipAddress: string; // Track IP for compliance
  
  @Column({ nullable: true })
  deviceInfo: string; // Track device
}
// app.module.ts
import { ConsentModule } from 'consent-core';
import { CustomConsentDefinition } from './entities/custom-consent-definition.entity';
import { CustomUserConsent } from './entities/custom-user-consent.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      // ... your database config
    }),
    ConsentModule.forRoot({
      consentDefinitionEntity: CustomConsentDefinition, // 👈 Your entity
      userConsentEntity: CustomUserConsent,             // 👈 Your entity
    }),
  ],
})
export class AppModule {}

What you get:

  • ✅ Your custom entity fields
  • ✅ Default REST API endpoints still work
  • ✅ Full backward compatibility
  • ⚠️ Important: Update migrations to include your custom columns

Scenario 3: Custom Controller Only 🎮

Use Case: You want your own API endpoints, authentication, and routing but keep the default entities.

// app.module.ts
import { ConsentModule } from 'consent-core';

@Module({
  imports: [
    ConsentModule.forRoot({
      disableControllers: true, // 👈 Disable default endpoints
    }),
  ],
  controllers: [MyCustomConsentController], // 👈 Your controller
})
export class AppModule {}
// controllers/my-custom-consent.controller.ts
import { Controller, Get, Post, Body, UseGuards, Req } from '@nestjs/common';
import { ConsentService, ConsentStatusDto, ConsentResponseDto } from 'consent-core';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { CurrentUser } from '../decorators/current-user.decorator';

@Controller('api/v2/user/consents') // 👈 Your custom route
@UseGuards(JwtAuthGuard)            // 👈 Your custom auth
export class MyCustomConsentController {
  constructor(private readonly consentService: ConsentService) {}

  @Get('my-status')
  async getMyConsentStatus(@CurrentUser() user): Promise<ConsentStatusDto> {
    return this.consentService.getStatus(user.id);
  }

  @Post('accept')
  async acceptConsent(
    @CurrentUser() user,
    @Body() dto: { consentDefinitionId: number }
  ): Promise<ConsentResponseDto> {
    return this.consentService.respond(
      user.id,
      dto.consentDefinitionId,
      'ACCEPTED',
      user.email, // actor for audit
    );
  }

  @Post('decline')
  async declineConsent(
    @CurrentUser() user,
    @Body() dto: { consentDefinitionId: number }
  ): Promise<ConsentResponseDto> {
    return this.consentService.respond(
      user.id,
      dto.consentDefinitionId,
      'REJECTED',
      user.email,
    );
  }

  @Post('remind-later')
  async remindLater(
    @CurrentUser() user,
    @Body() dto: { consentDefinitionId: number; days?: number }
  ) {
    return this.consentService.suppress(
      user.id,
      dto.consentDefinitionId,
      dto.days || 7, // Default 7 days
      user.email,
    );
  }
}

What you get:

  • ✅ Full control over routes and authentication
  • ✅ Use ConsentService and ConsentRepository directly
  • ✅ Default entities (or extend them)
  • ✅ Keep all business logic from the library

Scenario 4: Both Custom Entities and Controller 🚀

Use Case: Maximum flexibility - your own entities AND your own controller.

// app.module.ts
import { ConsentModule } from 'consent-core';
import { CustomConsentDefinition } from './entities/custom-consent-definition.entity';
import { CustomUserConsent } from './entities/custom-user-consent.entity';
import { MyCustomConsentController } from './controllers/my-custom-consent.controller';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      // ... your database config
    }),
    ConsentModule.forRoot({
      consentDefinitionEntity: CustomConsentDefinition, // 👈 Your entities
      userConsentEntity: CustomUserConsent,
      disableControllers: true,                         // 👈 No default endpoints
    }),
  ],
  controllers: [MyCustomConsentController],             // 👈 Your controller
})
export class AppModule {}

Custom entities with extra fields:

// entities/custom-consent-definition.entity.ts
import { Entity, Column } from 'typeorm';
import { ConsentDefinition } from 'consent-core';

@Entity('consent_definitions')
export class CustomConsentDefinition extends ConsentDefinition {
  @Column({ nullable: true })
  category: string;
  
  @Column({ nullable: true })
  legalBasis: string;
  
  @Column({ nullable: true })
  retentionPeriod: number;
}

Custom controller using your entities:

// controllers/my-custom-consent.controller.ts
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { ConsentService, ConsentRepository } from 'consent-core';
import { AdminGuard } from '../guards/admin.guard';

@Controller('admin/consents')
@UseGuards(AdminGuard) // Admin-only endpoints
export class MyCustomConsentController {
  constructor(
    private readonly consentService: ConsentService,
    private readonly consentRepository: ConsentRepository,
  ) {}

  @Get('definitions')
  async getAllDefinitions() {
    // Access your custom entity fields
    return this.consentRepository.findActiveDefinitions();
  }

  @Post('bulk-suppress')
  async bulkSuppressForUsers(@Body() dto: { userIds: number[]; days: number }) {
    const results = [];
    for (const userId of dto.userIds) {
      const status = await this.consentService.getStatus(userId);
      for (const item of status.consents) {
        if (item.status === 'PENDING') {
          await this.consentService.suppress(userId, item.id, dto.days, 'admin');
        }
      }
      results.push({ userId, suppressed: true });
    }
    return results;
  }
}

What you get:

  • ✅ Complete control over data structure
  • ✅ Complete control over API design
  • ✅ Full access to service and repository layers
  • ✅ Keep all the consent management logic

Scenario 5: Programmatic Usage (Service Layer Only) 🔨

Use Case: No REST API needed - use consent management in your business logic.

// app.module.ts
import { ConsentModule } from 'consent-core';

@Module({
  imports: [
    ConsentModule.forRoot({
      disableControllers: true, // 👈 No HTTP endpoints
    }),
  ],
})
export class AppModule {}
// services/onboarding.service.ts
import { Injectable } from '@nestjs/common';
import { ConsentService } from 'consent-core';

@Injectable()
export class OnboardingService {
  constructor(private readonly consentService: ConsentService) {}

  async canUserAccessFeature(userId: number): Promise<boolean> {
    const { mandatoryPending } = await this.consentService.getStatus(userId);
    return !mandatoryPending; // Block if mandatory consents pending
  }

  async completeUserOnboarding(userId: number, email: string) {
    const { consents } = await this.consentService.getStatus(userId);
    
    // Auto-accept non-mandatory consents
    for (const consent of consents) {
      if (!consent.mandatory && consent.status === 'PENDING') {
        await this.consentService.respond(
          userId,
          consent.id,
          'ACCEPTED',
          `onboarding:${email}`,
        );
      }
    }
  }

  async checkMarketingConsent(userId: number): Promise<boolean> {
    const { consents } = await this.consentService.getStatus(userId);
    const marketing = consents.find(c => c.key === 'marketing');
    return marketing?.status === 'ACCEPTED';
  }
}
// services/user-registration.service.ts
import { Injectable } from '@nestjs/common';
import { ConsentService } from 'consent-core';

@Injectable()
export class UserRegistrationService {
  constructor(private readonly consentService: ConsentService) {}

  async registerUser(userData: any): Promise<any> {
    // Create user account
    const user = await this.createUserAccount(userData);
    
    // Auto-accept mandatory terms during registration
    const { consents } = await this.consentService.getStatus(user.id);
    
    for (const consent of consents) {
      if (consent.mandatory && userData.acceptedTerms) {
        await this.consentService.respond(
          user.id,
          consent.id,
          'ACCEPTED',
          `registration:${userData.email}`,
        );
      }
    }
    
    return user;
  }

  private async createUserAccount(userData: any) {
    // Your user creation logic
    return { id: 123, email: userData.email };
  }
}

---

## 🗃️ Database Setup

### Option 1: Run Migration SQL

```bash
psql your_database < node_modules/consent-core/migrations.sql

Option 2: Copy Migration Content

See migrations.sql in the package for the complete schema.

Option 3: TypeORM Sync (Development Only)

TypeOrmModule.forRoot({
  synchronize: true, // Only in development
  entities: [ConsentDefinition, UserConsent],
})

🔌 API Reference

GET /consents/status

Description: Get all consent definitions with user's current status

Headers:

Authorization: Bearer <token>

Response (200):

{
  "mandatoryPending": boolean,  // Block app if true
  "consents": [
    {
      "id": 1,
      "key": "terms",
      "title": "Terms & Conditions",
      "content": "<p>By using our service...</p>",
      "contentType": "html",
      "version": "v1.0",
      "mandatory": true,
      "status": "PENDING" | "ACCEPTED" | "REJECTED",
      "validUntil": "2027-01-22T00:00:00.000Z" | null,
      "suppressedUntil": "2026-01-29T00:00:00.000Z" | null
    }
  ]
}

POST /consents/respond

Description: User accepts or rejects a consent

Headers:

Authorization: Bearer <token>

Request Body:

{
  "consentDefinitionId": number,
  "status": "ACCEPTED" | "REJECTED",
  "actor": string (optional) // e.g., user email
}

Response (200):

{
  "success": true,
  "expireAt": "2027-01-22T00:00:00.000Z" | null
}

POST /consents/suppress

Description: Temporarily hide consent ("Ask me later")

Headers:

Authorization: Bearer <token>

Request Body:

{
  "consentDefinitionId": number,
  "suppressDays": number,      // e.g., 7 for one week
  "actor": string (optional)
}

Response (200):

{
  "success": true,
  "suppressedUntil": "2026-01-29T00:00:00.000Z"
}

🎯 Frontend Integration

React Example

import { useEffect, useState } from 'react';

function ConsentModal() {
  const [status, setStatus] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchConsentStatus();
  }, []);

  const fetchConsentStatus = async () => {
    const res = await fetch('/consents/status', {
      headers: { Authorization: `Bearer ${token}` }
    });
    const data = await res.json();
    setStatus(data);
    setLoading(false);
  };

  const acceptConsent = async (consentId) => {
    await fetch('/consents/respond', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`
      },
      body: JSON.stringify({
        consentDefinitionId: consentId,
        status: 'ACCEPTED'
      })
    });
    fetchConsentStatus(); // Refresh
  };

  const suppressConsent = async (consentId) => {
    await fetch('/consents/suppress', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`
      },
      body: JSON.stringify({
        consentDefinitionId: consentId,
        suppressDays: 7
      })
    });
    fetchConsentStatus();
  };

  if (loading) return <div>Loading...</div>;
  if (!status.mandatoryPending) return null; // No consents needed

  return (
    <div className="modal">
      {status.consents
        .filter(c => c.status !== 'ACCEPTED')
        .map(consent => (
          <div key={consent.id}>
            <h3>{consent.title}</h3>
            <div dangerouslySetInnerHTML={{ __html: consent.content }} />
            {consent.mandatory && <span className="badge">Required</span>}
            <button onClick={() => acceptConsent(consent.id)}>
              Accept
            </button>
            {!consent.mandatory && (
              <button onClick={() => suppressConsent(consent.id)}>
                Ask me later
              </button>
            )}
          </div>
        ))}
    </div>
  );
}

🧪 Testing

import { Test } from '@nestjs/testing';
import { ConsentService } from 'consent-core';

describe('Consent Integration', () => {
  let service: ConsentService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      imports: [ConsentModule.forRoot()],
    }).compile();

    service = module.get<ConsentService>(ConsentService);
  });

  it('should return pending status for new user', async () => {
    const status = await service.getStatus(999);
    expect(status.mandatoryPending).toBe(true);
    expect(status.consents[0].status).toBe('PENDING');
  });

  it('should accept consent', async () => {
    const result = await service.respond(999, 1, 'ACCEPTED');
    expect(result.success).toBe(true);
    expect(result.expireAt).toBeDefined();
  });
});

🔒 Security Considerations

What consent-core Does NOT Handle

  • ❌ User authentication/login
  • ❌ Authorization/permissions
  • ❌ Rate limiting
  • ❌ CORS configuration
  • ❌ Input sanitization (beyond TypeScript validation)

Consumer Responsibilities

  • ✅ Apply authentication guards to endpoints
  • ✅ Validate user permissions
  • ✅ Implement rate limiting
  • ✅ Configure CORS properly
  • ✅ Sanitize user inputs
  • ✅ Set up audit logging
  • ✅ Implement proper error handling

Recommended Setup

@Controller('consents')
@UseGuards(JwtAuthGuard) // Authentication
@UseInterceptors(AuditLogInterceptor) // Logging
export class ConsentController {
  // ... endpoints
}

📊 Performance Tips

1. Add Caching

import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { ConsentService } from 'consent-core';

@Injectable()
export class CachedConsentService {
  constructor(
    private consentService: ConsentService,
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
  ) {}

  async getStatus(userId: number) {
    const cacheKey = `consent:status:${userId}`;
    
    let status = await this.cacheManager.get(cacheKey);
    if (status) return status;

    status = await this.consentService.getStatus(userId);
    await this.cacheManager.set(cacheKey, status, 300); // 5 min TTL
    
    return status;
  }

  async invalidateCache(userId: number) {
    await this.cacheManager.del(`consent:status:${userId}`);
  }
}

2. Database Indexes

Already included in migrations.sql:

  • user_id index (frequent lookups)
  • consent_definition_id index (joins)
  • Composite (user_id, consent_definition_id) index

🌐 Environment Configuration

// config.ts
export const consentConfig = {
  defaultValidityDays: 365,
  suppressionMaxDays: 30,
  mandatoryBlocksApp: true,
};

// Usage in custom controller
@Post('respond')
async respond(@Body() dto: any) {
  // Custom business logic
  if (dto.status === 'REJECTED' && isMandatory) {
    throw new BadRequestException('Cannot reject mandatory consent');
  }
  return this.consentService.respond(...);
}

📚 Additional Resources


🤝 Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License

MIT License - see LICENSE file for details


🆘 Support


⭐ Show Your Support

If this library helped you, please give it a ⭐️ on GitHub!


📦 NPM Publishing

# Build the project
npm run build

# Publish to npm (after authentication)
npm publish

Made with ❤️ by the consent-core contributors