Skip to content

bytepotato-ug/eu-dsa

Repository files navigation

eu-dsa

npm version CI License: MIT Node.js TypeScript

Open-source EU Digital Services Act compliance toolkit for Node.js platforms.

The EU Digital Services Act has been fully enforceable since February 2024. Every online platform serving EU users must submit Statements of Reasons to the EU Transparency Database, generate standardized transparency reports, implement notice-and-action mechanisms, and handle appeals — all with statutory deadlines.

There are 2M+ npm packages. Until now, zero addressed the DSA.

eu-dsa is the first open-source developer toolkit for DSA compliance. It handles the technical obligations so you can focus on building your platform.

Install

npm install eu-dsa

Requires Node.js 18+. TypeScript-first with full type safety. ESM-only.

Quick Start: Submit a Statement of Reasons

import {
  TransparencyDatabaseClient,
  SoRBuilder,
  deterministicPuid,
} from 'eu-dsa';

const client = new TransparencyDatabaseClient({
  token: process.env.EU_TRANSPARENCY_TOKEN!,
  sandbox: process.env.NODE_ENV !== 'production',
});

const sor = new SoRBuilder()
  .puid(deterministicPuid({
    platform: 'myapp',
    actionType: 'mod',
    referenceId: 'rpt-123',
  }))
  .contentRemoved()
  .illegalContent('§130 StGB', 'Incitement to hatred targeting ethnic minority group')
  .contentType('TEXT')
  .category('ILLEGAL_OR_HARMFUL_SPEECH')
  .dates({ content: '2024-06-15', application: '2024-06-16' })
  .facts('Content contained explicit incitement to hatred against an ethnic group.')
  .source('ARTICLE_16')
  .automatedDetection(true)
  .automatedDecision('PARTIALLY')
  .territorialScope(['DE'])
  .build();

const result = await client.submitStatement(sor);
console.log(`Submitted: ${result.uuid}`);

Batch Submission

import { TransparencyDatabaseClient, SoRBuilder, randomPuid } from 'eu-dsa';

const client = new TransparencyDatabaseClient({
  token: process.env.EU_TRANSPARENCY_TOKEN!,
});

const statements = moderationDecisions.map((decision) =>
  new SoRBuilder()
    .puid(randomPuid('myapp'))
    .contentRemoved()
    .illegalContent(decision.legalGround, decision.explanation)
    .contentType('TEXT')
    .category('ILLEGAL_OR_HARMFUL_SPEECH')
    .dates({ content: decision.contentDate, application: decision.decisionDate })
    .facts(decision.facts)
    .source('ARTICLE_16')
    .automatedDetection(decision.aiDetected)
    .automatedDecision(decision.aiDecided ? 'PARTIALLY' : 'NOT_AUTOMATED')
    .build()
);

// Auto-chunks into 100-item batches per EU API limits
const result = await client.submitStatements(statements);
console.log(`Submitted ${result.statements.length} statements`);

Platform Category Mapping

Map your internal moderation categories to the EU's standardized taxonomy:

import {
  createPlatformMapper,
  SoRBuilder,
  Category,
  CategorySpecification,
  ContentType,
} from 'eu-dsa';

const mapper = createPlatformMapper({
  platformName: 'myapp',
  categories: {
    hate_speech: {
      euCategory: Category.ILLEGAL_OR_HARMFUL_SPEECH,
      euSpecifications: [CategorySpecification.HATE_SPEECH, CategorySpecification.INCITEMENT_VIOLENCE_HATRED],
      defaultGround: 'ILLEGAL_CONTENT',
      legalGround: '§130 StGB',
    },
    terrorism: {
      euCategory: Category.RISK_FOR_PUBLIC_SECURITY,
      euSpecifications: [CategorySpecification.TERRORIST_CONTENT],
      defaultGround: 'ILLEGAL_CONTENT',
      legalGround: 'EU Reg 2021/784',
    },
    spam: {
      euCategory: Category.SCAMS_AND_FRAUD,
      defaultGround: 'INCOMPATIBLE_CONTENT',
    },
  },
  defaultContentTypes: [ContentType.TEXT],
  defaultTerritorialScope: ['DE'],
});

const builder = new SoRBuilder();
mapper(builder, 'hate_speech');
// Builder is now pre-populated with EU category, specifications, and legal ground

PUID Generation

Every Statement of Reasons needs a Platform-Unique Identifier (PUID):

import {
  deterministicPuid,
  randomPuid,
  hashedPuid,
  timestampPuid,
  isValidPuid,
} from 'eu-dsa';

// Deterministic — same inputs always produce the same PUID
deterministicPuid({ platform: 'myapp', actionType: 'mod', referenceId: '123' });
// → 'myapp-mod-123'

// Random — UUID-based
randomPuid('myapp');
// → 'myapp-a1b2c3d4-e5f6-7890-abcd-ef1234567890'

// Hashed — SHA-256 prefix for privacy
await hashedPuid('myapp', 'user-42', 'post-789');
// → 'myapp-a1b2c3d4e5f67890'

// Timestamp-based
timestampPuid('myapp');
// → 'myapp-20240616-a1b2c3d4'

// Validate any PUID
isValidPuid('myapp-mod-123'); // true
isValidPuid('invalid puid!'); // false (spaces and special chars not allowed)

GDPR Pseudonymization

Strip personal data before submitting SoRs to the EU Transparency Database:

import {
  pseudonymizeUserId,
  sanitizeForSubmission,
  stripIpAddresses,
  stripEmails,
} from 'eu-dsa';

// Hash user IDs
const anonId = pseudonymizeUserId('user-42', { salt: process.env.PSEUDO_SALT! });
// → 'user_a1b2c3d4e5f6'

// Strip all PII from free-text fields in one pass
const cleanText = sanitizeForSubmission(
  'User john@example.com (IP: 192.168.1.1) posted hate speech mentioning @victim',
  { stripIps: true, stripEmailAddresses: true, stripUserMentions: true }
);
// → 'User [EMAIL_REDACTED] (IP: [IP_REDACTED]) posted hate speech mentioning [USER_REDACTED]'

Offline Queue

Handle API downtime gracefully with automatic queuing:

import { TransparencyDatabaseClient, InMemoryQueue } from 'eu-dsa';

const client = new TransparencyDatabaseClient({
  token: process.env.EU_TRANSPARENCY_TOKEN!,
});

const queue = new InMemoryQueue();
client.setQueue(queue);

// Automatically queues on retryable errors (503, timeout, etc.)
const result = await client.submitOrQueue(sor);
if ('status' in result && result.status === 'pending') {
  console.log('API unavailable, statement queued for retry');
}

// Later, flush the queue
const { submitted, failed } = await client.flushQueue();

Interceptors (Audit Logging)

Log every API call for compliance audit trails:

import { TransparencyDatabaseClient } from 'eu-dsa';

const client = new TransparencyDatabaseClient({
  token: process.env.EU_TRANSPARENCY_TOKEN!,
  interceptors: {
    request: [(ctx) => {
      console.log(`[DSA] ${ctx.method} ${ctx.url} requestId=${ctx.requestId}`);
      return ctx;
    }],
    response: [(ctx) => {
      console.log(`[DSA] ${ctx.statusCode} in ${ctx.durationMs}ms requestId=${ctx.requestId}`);
    }],
  },
});

Event System

React to DSA lifecycle events:

import { createDsaEventEmitter } from 'eu-dsa';

const emitter = createDsaEventEmitter();

emitter.on('sor.submitted', ({ submission, response }) => {
  console.log(`SoR ${response.uuid} submitted for PUID ${submission.puid}`);
});

emitter.on('sor.submission_failed', ({ submission, error }) => {
  alertOps(`SoR submission failed for ${submission.puid}: ${error.message}`);
});

emitter.on('api.rate_limited', ({ retryAfterMs }) => {
  console.warn(`Rate limited, retry after ${retryAfterMs}ms`);
});

Error Handling

All errors are typed with programmatic code fields:

import {
  DsaValidationError,
  DsaApiError,
  DsaAuthError,
  DsaPuidConflictError,
  DsaRateLimitError,
  DsaNetworkError,
} from 'eu-dsa';

try {
  await client.submitStatement(sor);
} catch (err) {
  if (err instanceof DsaValidationError) {
    // Zod validation failed before API call
    console.error('Invalid SoR:', err.fieldErrors);
  } else if (err instanceof DsaPuidConflictError) {
    // PUID already exists — skip or update
    console.warn(`Duplicate PUID: ${err.puid}`);
  } else if (err instanceof DsaAuthError) {
    // 401/403 — check your API token
    console.error('Auth failed:', err.message);
  } else if (err instanceof DsaRateLimitError) {
    // 429 — retry after delay
    console.warn(`Rate limited, retry after ${err.retryAfterMs}ms`);
  } else if (err instanceof DsaNetworkError) {
    // Timeout or connection error
    console.error('Network error:', err.message);
  } else if (err instanceof DsaApiError) {
    // Other API error (422, 500, etc.)
    console.error(`API error ${err.statusCode}:`, err.message);
  }
}

What This Library Covers vs. What You Build

eu-dsa handles the protocol layer — talking to the EU, validating data, building compliant submissions. Your platform still needs application-level code to tie it all together.

Layer eu-dsa provides You build
EU API HTTP client, auth, retries, rate limiting, batch chunking Client initialization, API token management
Statements of Reasons SoRBuilder fluent API, Zod validation, 160+ EU enums Database schema, SoR creation logic, user notification
Category Mapping createPlatformMapper(), full EU taxonomy Mapping your categories to EU categories
GDPR sanitizeForSubmission(), IP/email/mention stripping Deciding what fields to sanitize
Submission Submit/batch/queue to EU Transparency Database Background job queue (BullMQ, SQS, etc.), error recovery
Notices (Art. 16) State machine, deadline calculator, priority scoring Database persistence, admin review UI
Appeals (Art. 20) Workflow state machine, 6-month window calculator Complaint database, different-reviewer enforcement, user notification
Trusted Flaggers (Art. 22) Priority multiplier, accuracy evaluation Accuracy tracking in your database, status management
Reports (Art. 15/24) Report generator, CSV/JSON/Markdown formatters TransparencyDataProvider implementation (queries your database)

In short: eu-dsa is ~20% of the work. The remaining ~80% is application code specific to your platform, database, and infrastructure.

See the Integration Guide for a step-by-step walkthrough with code examples, and the example Prisma schema for a ready-to-copy database schema.

What's Covered

DSA Article Obligation Module Status
Art. 17 Statement of Reasons sor/ ✅ v0.1.0
Art. 24(5) Submit SoRs to EU Transparency Database api/ ✅ v0.1.0
Art. 16 Notice-and-action mechanism notice/ ✅ v0.2.0
Art. 20 Internal complaint-handling appeals/ ✅ v0.2.0
Art. 22 Trusted flagger processing notice/ ✅ v0.2.0
Art. 15/24/42 Transparency reports reports/ ✅ v0.5.0

Subpath Exports

Import only what you need:

import { TransparencyDatabaseClient } from 'eu-dsa/api';
import { SoRBuilder } from 'eu-dsa/sor';
import { sorSubmissionSchema } from 'eu-dsa/schemas';
import { createDsaEventEmitter } from 'eu-dsa/events';

Links

Roadmap

  • v0.1.0 — Core schemas, API client, SoR builder, PUID generation, pseudonymization, platform mapper
  • v0.2.0 — Notice engine, appeals handler, event system integration, storage adapter
  • v0.5.0 — Transparency reports (CSV/JSON/Markdown per EU template), typed event emitter
  • v1.0.0 — Stable API, 97%+ test coverage, full documentation, integration guide
  • Post-1.0 — Express/Fastify middleware, Prisma storage adapter, Mastodon adapter

License

MIT

Disclaimer

This package automates the technical obligations of the EU Digital Services Act (Regulation (EU) 2022/2065). It does not constitute legal advice and does not replace consultation with qualified legal professionals. Compliance with the DSA requires organizational, legal, and technical measures — this package addresses only the technical components. You are responsible for ensuring your platform's overall compliance with applicable laws.

About

Open-source EU Digital Services Act compliance toolkit — Statement of Reasons submission, transparency reports, notice-and-action engine, and appeals handling for Node.js platforms

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors