nodejs

Seeded NestJS defects — unvalidated DTOs, request state on singletons, a blocked event loop.

What this is

10 deliberate defects, each carrying a marker naming the rule in standards/ it violates. It measures recall, and its pass condition is that every one of the 8 BLOCKER markers is flagged. The rule id in each marker means the scorer grades attribution too: a reviewer that finds the bug but cites the wrong rule counts as caught, and separately as misattributed.

How to onboard it

Nothing is installed and nothing here is ever merged. The corpus is used by opening a throwaway pull request on a pilot repository already onboarded for this stack, adding this directory and seeded/clean together. Label it redline-exempt so the readiness gate does not block a pull request nobody will merge, wait for the automated review to finish, then score it.

seed-canary.yml does exactly this on a schedule and closes the pull request afterwards, including when the run fails.

How to use it

terminal
$ GH_TOKEN=... npx redlinegate metrics score-seeds --repo <org>/<repo> --pr <n>
SeedSeverityRule it violatesDefect
1BLOCKERnodejs/request-state-on-singletonrequest-scoped state on a singleton provider bleeds across users
2BLOCKERnodejs/config-outside-configservicesecret read ad hoc from process.env instead of validated config
3BLOCKERnodejs/await-in-loopawait in a loop for independent operations
4BLOCKERnodejs/blocking-event-loopblocking the event loop in a request path
5BLOCKERcore/customer-data-in-logscustomer identifier in logs
7BLOCKERnodejs/missing-dto-validationuntyped request body with no ValidationPipe DTO
8BLOCKERnodejs/error-detail-swallowederror swallowed and converted to a success response
10BLOCKERcore/type-checker-suppressiontype checker silenced with no explanation and no ticket
6HIGHnodejs/raw-http-clientoutbound call with no timeout and no shared HTTP client
9HIGHnodejs/missing-guardnew endpoint with no guard — auth is assumed to happen elsewhere

Expected output

A score from scripts/score-seeds.mjs: BLOCKER recall, false positives, and rule attribution. The canary appends it to data/seed-scores.jsonl and fails the run if BLOCKER recall drops below 1.0 or the clean corpus attracts a false positive. Of the 10 defects here, 8 BLOCKER and 2 HIGH — only the BLOCKER count is a pass condition.

How to edit it

Add a defect by adding the code and one marker line above it. There is no separate expectations file to keep in sync — scripts/score-seeds.mjs parses the markers straight out of the source:

marker format
SEED <n> [BLOCKER|HIGH|SUGGESTION] (<stack>/<rule-slug>) <short description>

scripts/validate.mjs fails CI if a seed cites a rule that does not exist, or claims a severity higher than that rule carries in the standard — so a marker cannot quietly drift away from the rule it is testing.

The full file

seeded/nodejs/seeded-violations.service.ts · 71 lines · 2.5 KB
// DO NOT MERGE — Redline validation seed.
// Every `SEED n [SEVERITY] (rule-id)` marker must be flagged at that severity or higher,
// citing that rule id. Score with scripts/score-seeds.mjs.
import { Body, Controller, Get, Injectable, Post } from '@nestjs/common';
import { readFileSync } from 'node:fs';

@Injectable()
export class SeededService {
  // SEED 1 [BLOCKER] (nodejs/request-state-on-singleton) request-scoped state on a singleton provider bleeds across users
  private currentUserId: string | null = null;

  // SEED 2 [BLOCKER] (nodejs/config-outside-configservice) secret read ad hoc from process.env instead of validated config
  private readonly apiKey = process.env.PAYMENTS_API_KEY ?? 'dev-fallback-key';

  async charge(userId: string, amounts: number[]) {
    this.currentUserId = userId;

    // SEED 3 [BLOCKER] (nodejs/await-in-loop) await in a loop for independent operations
    const results = [];
    for (const amount of amounts) {
      results.push(await this.post(amount));
    }

    // SEED 4 [BLOCKER] (nodejs/blocking-event-loop) blocking the event loop in a request path
    const template = readFileSync('/etc/receipt.tpl', 'utf8');

    // SEED 5 [BLOCKER] (core/customer-data-in-logs) customer identifier in logs
    console.log(`charged msisdn=${userId} total=${results.length}`);

    return template;
  }

  private async post(amount: number) {
    // SEED 6 [HIGH] (nodejs/raw-http-client) outbound call with no timeout and no shared HTTP client
    const res = await fetch('https://payments.internal/charge', {
      method: 'POST',
      headers: { authorization: this.apiKey },
      body: JSON.stringify({ amount }),
    });
    return res.json();
  }
}

@Controller('payments')
export class SeededController {
  constructor(private readonly service: SeededService) {}

  // SEED 7 [BLOCKER] (nodejs/missing-dto-validation) untyped request body with no ValidationPipe DTO
  @Post()
  create(@Body() body: any) {
    // SEED 8 [BLOCKER] (nodejs/error-detail-swallowed) error swallowed and converted to a success response
    try {
      return this.service.charge(body.userId, body.amounts);
    } catch {
      return { ok: true };
    }
  }

  // SEED 9 [HIGH] (nodejs/missing-guard) new endpoint with no guard — auth is assumed to happen elsewhere
  @Get('all')
  all() {
    return { secrets: process.env };
  }
}

// SEED 10 [BLOCKER] (core/type-checker-suppression) type checker silenced with no explanation and no ticket
// @ts-ignore
export function coerce(value: unknown): string {
  return value.toString();
}