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

the-stripper

v0.1.1

Published

Strip and reshape JSON safely before sending it to the client.

Readme

The Stripper

npm version

The Stripper is a small JavaScript/TypeScript library for cleaning JSON before you send it back from a server action, API route, or Cloud Run function.

Think of JSON like boxes inside boxes:

  • A key is the label on a box: "secret", "user_email", "docs"
  • A value is what is inside the box
  • A path is the route to a box: "docs.0.report_status"
  • A sanitised response is the clean box you can safely return

The Stripper helps you do two simple things:

  • Remove unsafe pieces from a JSON object
  • Build a new clean JSON shape from a raw JSON object

The library is:

  • Pure and synchronous
  • Dependency-free at runtime
  • Safe for server-side usage in Next.js/GCP
  • Non-mutating for arrays and plain objects
  • Conservative with non-plain objects such as Date

Production Safety Defaults

The Stripper is meant to sit between raw server data and the JSON you return to a browser.

So it has a few safety rules built in:

  • It removes dangerous object keys: __proto__, constructor, and prototype.
  • It creates clean objects without inherited prototype values.
  • getPath only reads real keys on the object, not hidden inherited keys.
  • RegExp key patterns are checked safely, so /secret/g and /secret/y do not skip every second matching key.
  • ** wildcard paths are matched with caching, so complicated wildcard paths do not explode into very slow recursive work.

Simple version:

The Stripper should return clean JSON data, not magic prototype behavior.

Can It Clean Any JSON?

Yes, it can clean any JSON-like value.

JSON-like means:

  • objects
  • arrays
  • strings
  • numbers
  • booleans
  • null

It can clean small JSON, big JSON, nested JSON, and arrays of JSON rows.

But it is not magic. You must tell it the cleaning rule.

For example:

const safe = stripJson(raw, {
  remove: {
    keys: ["secret", "user_email"],
  },
});

Simple rule:

The Stripper can clean the JSON, but you decide what is unsafe.

Important boundaries:

  • It does not guess which fields are sensitive.
  • It preserves non-plain objects like Date instead of opening them.
  • It throws JsonStripperError for circular JSON, invalid paths, too-deep JSON, or too-large JSON.
  • It does not mutate the original plain object or array.
  • It always blocks prototype-pollution keys, even if you forgot to list them in remove.keys.

Install

npm install the-stripper

Import

import {
  stripJson,
  shapeJson,
  shapeDocsResponse,
  createJsonStripper,
  getPath,
  parsePath,
  matchesPath,
  JsonStripperError,
} from "the-stripper";

TypeScript types ship with the package — no @types/the-stripper needed. The package is ESM-only (import, not require).

Function Navigation

Click a function name to jump to its explanation and example.

| Function | Use it when | | --- | --- | | stripJson(value, options) | You already like the JSON shape and want to remove unsafe fields. | | shapeJson(input, shape) | You want to build a new clean JSON shape from raw JSON. | | shapeDocsResponse(result, options) | Your API response looks like { docs: [...], pagination: ... }. | | createJsonStripper(options) | You want to save one cleaning rule and reuse it. | | getPath(value, path) | You want to safely read one nested value. | | parsePath(path) | You want to turn a path string into path pieces. | | matchesPath(actualPath, rulePath) | You want to check if a real path matches a wildcard path rule. | | JsonStripperError | You want to catch library errors cleanly. | | TypeScript Types | You want to see the exported type shapes. |

Run The Examples

The commands below (npm run example, npm run portfolio:sample, node attempts-corner.js) only work inside a clone of this repository — they build and import the local dist/ output, not the published the-stripper package. In your own project, import ... from "the-stripper" as shown above.

example.js shows every public function using this raw JSON file:

samples/attempts-corner.raw.json

Run:

npm run example

The output is printed in separate labelled sections.

Run The Portfolio Sample

portfolio-example.js uses this sample file:

samples/projects.json

Run:

npm run portfolio:sample

Small snippet:

import { stripJson } from "the-stripper";

const cleanJson = stripJson(rawJson, {
  remove: {
    keys: ["_id", "end"],
  },
});

That removes every _id and every end key, wherever they are hiding.

If you only want to remove a specific field, like each project's first image, use a path:

const cleanJson = stripJson(rawJson, {
  remove: {
    keys: ["_id"],
    paths: ["pageProps.projects.*.images.0"],
  },
});

Basic Words

Key

A key is the property name.

{
  "user_email": "[email protected]"
}

Here, the key is user_email.

Path

A path tells the library where to go.

{
  "docs": [
    {
      "report_status": "evaluated"
    }
  ]
}

The path to "evaluated" is:

docs.0.report_status

That means:

  • Go into docs
  • Pick item 0
  • Read report_status

*

* means "one thing here".

docs.*.user_email

That means:

  • Go into docs
  • For each item inside docs
  • Remove or match user_email

**

** means "look anywhere below here".

**.secret

That means:

  • Look at the whole object
  • No matter how deep secret is
  • Match it

stripJson(value, options)

stripJson takes JSON in, removes unwanted pieces, and gives clean JSON back.

It does not change the original object.

Remove By Key Name With remove.keys

Use this when a key is unsafe everywhere.

Input:

const raw = {
  name: "Asha",
  secret: "hide-this",
  profile: {
    email: "[email protected]",
    secret: "hide-this-too",
  },
};

Code:

const safe = stripJson(raw, {
  remove: {
    keys: ["secret"],
  },
});

Output:

{
  "name": "Asha",
  "profile": {
    "email": "[email protected]"
  }
}

Simple rule:

If any key is named "secret", remove it from anywhere.

Remove By Key Pattern With remove.keyPatterns

Use this when many unsafe keys start or end the same way.

Input:

const raw = {
  attempt_id: "attempt-1",
  change_request_id: "internal-1",
  change_request_status: "pending",
  title: "Example Assessment",
};

Code:

const safe = stripJson(raw, {
  remove: {
    keyPatterns: ["change_request_*"],
  },
});

Output:

{
  "attempt_id": "attempt-1",
  "title": "Example Assessment"
}

Simple rule:

"change_request_*" means remove keys that start with "change_request_".

The * inside a key pattern means "any text".

More examples:

stripJson(raw, {
  remove: {
    keyPatterns: ["*_token", /^private_/],
  },
});

This removes keys like:

debug_token
auth_token
private_note
private_payload

Remove By Exact Path With remove.paths

Use this when only one exact place is unsafe.

Input:

const raw = {
  report_metadata: {
    result: {
      total_user_score: 2,
      score_by_question: {
        q1: 1,
      },
    },
  },
};

Code:

const safe = stripJson(raw, {
  remove: {
    paths: ["report_metadata.result.score_by_question"],
  },
});

Output:

{
  "report_metadata": {
    "result": {
      "total_user_score": 2
    }
  }
}

Simple rule:

Walk this exact route and remove the thing there.

Remove From Every Array Item With *

Use * when the unsafe field appears inside many array rows.

Input:

const raw = {
  docs: [
    { attempt_id: "a1", user_email: "[email protected]" },
    { attempt_id: "a2", user_email: "[email protected]" },
  ],
};

Code:

const safe = stripJson(raw, {
  remove: {
    paths: ["docs.*.user_email"],
  },
});

Output:

{
  "docs": [
    {
      "attempt_id": "a1"
    },
    {
      "attempt_id": "a2"
    }
  ]
}

Simple rule:

Inside docs, remove user_email from every item.

Remove From Anywhere Deep With **

Use ** when the unsafe field can be hiding at any depth.

Input:

const raw = {
  secret: "top",
  assessment_metadata: {
    totp: {
      secret: "deep",
    },
  },
};

Code:

const safe = stripJson(raw, {
  remove: {
    paths: [["**", "secret"]],
  },
});

Output:

{
  "assessment_metadata": {
    "totp": {}
  }
}

Simple rule:

Find any path that ends with secret and remove it.

Use A Path Array When A Key Has A Dot

String paths split on dots. So if the real key contains a dot, use a path array.

Input:

const raw = {
  "metadata.with.dot": {
    secret: "hide",
    safe: "keep",
  },
};

Code:

const safe = stripJson(raw, {
  remove: {
    paths: [["metadata.with.dot", "secret"]],
  },
});

Output:

{
  "metadata.with.dot": {
    "safe": "keep"
  }
}

Simple rule:

Use an array path when one key name has a dot inside it.

Keep Only Some Paths With keep.paths

Use keep.paths when you want to start with nothing and copy only safe fields.

Input:

const raw = {
  docs: [
    {
      assessment_title: "Example Assessment",
      attempt_id: "attempt-1",
      user_email: "[email protected]",
    },
  ],
  pagination: {
    total: 1,
    page: 1,
  },
  debug: {
    trace_id: "internal",
  },
};

Code:

const safe = stripJson(raw, {
  keep: {
    paths: [
      "docs.*.assessment_title",
      "docs.*.attempt_id",
      "pagination",
    ],
  },
});

Output:

{
  "docs": [
    {
      "assessment_title": "Example Assessment",
      "attempt_id": "attempt-1"
    }
  ],
  "pagination": {
    "total": 1,
    "page": 1
  }
}

Simple rule:

Only copy the paths I listed. Leave everything else behind.

Safety Limits

Use maxDepth to stop JSON that is too deeply nested.

Use maxNodes to stop JSON that is too large.

const safe = stripJson(raw, {
  remove: {
    keys: ["secret"],
  },
  maxDepth: 50,
  maxNodes: 100000,
});

Default values:

maxDepth = 50
maxNodes = 100000

If the JSON is circular, too deep, too large, or has a bad path, The Stripper throws JsonStripperError.

shapeJson(input, shape)

shapeJson builds a brand-new clean object.

Use it when you want to say:

Take these exact fields from the raw object and put them into this new shape.

Input:

const raw = {
  assessment_title: "Example Assessment",
  assessment_slug: "example-assessment",
  assessment_metadata: {
    report: {
      type: "summary_with_response",
    },
  },
  report_metadata: {
    result: {
      total_user_score: 2,
      max_possible_score: 30,
      score_percentage: 7,
    },
  },
  user_email: "[email protected]",
};

Code:

const safe = shapeJson(raw, {
  title: "assessment_title",
  slug: "assessment_slug",
  report_type: "assessment_metadata.report.type",
  score: {
    total_user_score: "report_metadata.result.total_user_score",
    max_possible_score: "report_metadata.result.max_possible_score",
    score_percentage: "report_metadata.result.score_percentage",
  },
  label: (row) =>
    `${row.report_metadata.result.total_user_score}/${row.report_metadata.result.max_possible_score}`,
  missing_value: null,
});

Output:

{
  "title": "Example Assessment",
  "slug": "example-assessment",
  "report_type": "summary_with_response",
  "score": {
    "total_user_score": 2,
    "max_possible_score": 30,
    "score_percentage": 7
  },
  "label": "2/30",
  "missing_value": null
}

Simple rules:

  • A string path copies a value from that path
  • A nested object creates a nested output object
  • A function can calculate a new value
  • null writes null
  • Anything not listed in the shape is not returned

Shape An Array

Use { from, each } when the raw JSON has an array.

Input:

const raw = {
  docs: [
    {
      assessment_title: "Assessment One",
      attempt_id: "a1",
      user_email: "[email protected]",
    },
    {
      assessment_title: "Assessment Two",
      attempt_id: "a2",
      user_email: "[email protected]",
    },
  ],
};

Code:

const safe = shapeJson(raw, {
  docs: {
    from: "docs",
    each: {
      assessment_title: "assessment_title",
      attempt_id: "attempt_id",
    },
  },
});

Output:

{
  "docs": [
    {
      "assessment_title": "Assessment One",
      "attempt_id": "a1"
    },
    {
      "assessment_title": "Assessment Two",
      "attempt_id": "a2"
    }
  ]
}

Simple rule:

For each item in docs, build this smaller item.

shapeDocsResponse(result, options)

shapeDocsResponse is for the common API shape:

{
  docs: [...],
  pagination: {...}
}

It does three things:

  • Reads the docs array
  • Maps each row into a clean row
  • Keeps pagination if pagination exists

Input:

const rawResponse = {
  docs: [
    {
      assessment_title: "Example Assessment",
      assessment_slug: "example-assessment",
      attempt_id: "attempt-1",
      assessment_metadata: {
        report: {
          type: "summary_with_response",
        },
      },
      user_email: "[email protected]",
      secret: "hide",
    },
  ],
  pagination: {
    total: 1,
    page: 1,
  },
};

Code:

const safe = shapeDocsResponse(rawResponse, {
  docsPath: "docs",
  paginationPath: "pagination",
  mapRow: (row) => ({
    assessment_title: row.assessment_title,
    assessment_slug: row.assessment_slug,
    attempt_id: row.attempt_id,
    report_type: getPath(row, "assessment_metadata.report.type"),
  }),
  finalStrip: {
    remove: {
      keys: ["secret", "user_email"],
    },
  },
});

Output:

{
  "docs": [
    {
      "assessment_title": "Example Assessment",
      "assessment_slug": "example-assessment",
      "attempt_id": "attempt-1",
      "report_type": "summary_with_response"
    }
  ],
  "pagination": {
    "total": 1,
    "page": 1
  }
}

Simple rule:

Clean every row in docs, then return docs and pagination.

Options:

  • docsPath: where the docs array lives. Default is "docs"
  • paginationPath: where pagination lives. Default is "pagination"
  • mapRow: function that returns one clean row
  • finalStrip: extra stripJson cleanup after mapping

If the raw object has no pagination, the output has only docs.

createJsonStripper(options)

createJsonStripper saves a cleaning rule so you can use it again and again.

Code:

const removeSecrets = createJsonStripper({
  remove: {
    keys: [
      "totp",
      "secret",
      "score_by_question",
      "user_id",
      "user_email",
      "author_email",
    ],
    keyPatterns: ["change_request_*"],
  },
});

const safeOne = removeSecrets(rawOne);
const safeTwo = removeSecrets(rawTwo);

Simple rule:

Make one cleaner. Use that cleaner on many JSON objects.

getPath(value, path)

getPath safely reads one value from inside a JSON object.

Input:

const raw = {
  docs: [
    {
      report_status: "evaluated",
    },
  ],
};

Code:

const reportStatus = getPath(raw, "docs.0.report_status");

Output:

"evaluated";

You can also use a path array:

const reportStatus = getPath(raw, ["docs", 0, "report_status"]);

If the path is missing, getPath returns undefined.

Important:

getPath does not use * or **.

It reads one exact path only.

parsePath(path)

parsePath turns a path into path pieces.

Code:

const path = parsePath("docs.*.report_status");

Output:

["docs", "*", "report_status"];

Use it when you want the library to check that a path is valid.

Bad paths throw JsonStripperError:

parsePath("");
parsePath("docs..report_status");

Simple rule:

Take a path string and split it into pieces.

matchesPath(actualPath, rulePath)

matchesPath answers yes or no:

Does this real path match this rule path?

Code:

const matched = matchesPath(
  ["docs", 0, "report_status"],
  ["docs", "*", "report_status"],
);

Output:

true;

Another example:

matchesPath(
  ["assessment_metadata", "totp", "secret"],
  ["**", "secret"],
);

Output:

true;

Simple rules:

  • * matches one piece
  • ** matches many pieces, or no pieces
  • Normal text must match the same normal text

JsonStripperError

JsonStripperError is the error class used by this library.

Catch it when you want to handle Stripper problems cleanly.

try {
  const safe = stripJson(raw, {
    remove: {
      paths: ["docs..bad_path"],
    },
  });
} catch (error) {
  if (error instanceof JsonStripperError) {
    console.error("The Stripper could not clean this JSON:", error.message);
  } else {
    throw error;
  }
}

It can happen when:

  • A path is empty or invalid
  • JSON is circular
  • JSON is deeper than maxDepth
  • JSON has more nodes than maxNodes

Full Attempts Corner Style Example

This is the pattern for a server action file.

The library stays universal. The server action decides the product-specific response shape.

The local demo file can also show the clean contract:

node attempts-corner.js

It prints two clean sections:

  • non-published clean after
  • published clean after

If you pass a raw JSON file path, it prints only the clean JSON response:

node attempts-corner.js samples/attempts-corner.raw.json
"use server";

import { getPath, shapeDocsResponse } from "the-stripper";

const SAFE_REPORT_RESULT_PATHS = {
  totalUserScore: "report_metadata.result.total_user_score",
  scorePercentage: "report_metadata.result.score_percentage",
  maxPossibleScore: "report_metadata.result.max_possible_score",
};

const ASSESSMENT_RAW_INTERNAL_KEYS = [
  "assessment_id",
  "assessment_created_at",
  "assessment_metadata",
];

const ATTEMPT_ACTOR_RAW_KEYS = [
  "user_id",
  "user_email",
  "author_email",
];

const ATTEMPT_TIMING_AND_STATE_RAW_KEYS = [
  "archived_at",
  "attempt_start_time",
  "attempt_calculated_end_time",
  "is_attempt_closed",
  "planned_assessment_id",
  "is_plagiarised",
  "report_updated_at",
];

const SECRET_RAW_KEYS = ["totp", "secret"];

const REPORT_RESULT_INTERNAL_KEYS = [
  "total_questions",
  "correct_questions",
  "wrong_questions",
  "percentage",
  "answered_questions",
  "unanswered_questions",
];

const QUESTION_SCORE_DETAIL_KEYS = [
  "score_by_question",
  "max_points",
  "max_negative_points",
  "user_score",
];

const FINAL_STRIP = {
  remove: {
    keys: [
      ...ASSESSMENT_RAW_INTERNAL_KEYS,
      ...ATTEMPT_ACTOR_RAW_KEYS,
      ...ATTEMPT_TIMING_AND_STATE_RAW_KEYS,
      ...SECRET_RAW_KEYS,
      ...REPORT_RESULT_INTERNAL_KEYS,
      ...QUESTION_SCORE_DETAIL_KEYS,
    ],
    keyPatterns: ["change_request_*"],
  },
};

export async function shapeAttemptsCornerAssessmentResponse(rawResponse) {
  return shapeDocsResponse(rawResponse, {
    mapRow: mapAttemptsCornerAssessmentRow,
    finalStrip: FINAL_STRIP,
  });
}

function mapAttemptsCornerAssessmentRow(row) {
  const reportStatus = getPath(row, "report_status") ?? null;
  const isPublished = String(reportStatus).toLowerCase() === "published";
  const baseRow = {
    assessment_title: getPath(row, "assessment_title") ?? null,
    assessment_slug: getPath(row, "assessment_slug") ?? null,
    attempt_id: getPath(row, "attempt_id") ?? null,
    attempt_closed_at: getPath(row, "attempt_closed_at") ?? null,
    report_status: reportStatus,
    report_type: getPath(row, "assessment_metadata.report.type") ?? null,
  };

  if (!isPublished) {
    return {
      ...baseRow,
      report_metadata: null,
    };
  }

  return {
    ...baseRow,
    report_metadata: {
      result: {
        total_user_score:
          getPath(row, SAFE_REPORT_RESULT_PATHS.totalUserScore) ?? null,
        score_percentage:
          getPath(row, SAFE_REPORT_RESULT_PATHS.scorePercentage) ?? null,
        max_possible_score:
          getPath(row, SAFE_REPORT_RESULT_PATHS.maxPossibleScore) ?? null,
      },
    },
  };
}

For a non-published report, the row becomes:

{
  "assessment_title": "Example Assessment",
  "assessment_slug": "example-assessment",
  "attempt_id": "attempt-1",
  "attempt_closed_at": "2026-06-20T11:12:25.422Z",
  "report_status": "evaluated",
  "report_type": "summary_with_response",
  "report_metadata": null
}

For a published report, the row becomes:

{
  "assessment_title": "Example Assessment",
  "assessment_slug": "example-assessment",
  "attempt_id": "attempt-1",
  "attempt_closed_at": "2026-06-20T11:12:25.422Z",
  "report_status": "published",
  "report_type": "summary_with_response",
  "report_metadata": {
    "result": {
      "total_user_score": 2,
      "score_percentage": 7,
      "max_possible_score": 30
    }
  }
}

TypeScript Types

type StripPathSegment = string | number | "*" | "**";
type StripPath = string | readonly StripPathSegment[];

type StripJsonOptions = {
  remove?: {
    keys?: readonly string[];
    keyPatterns?: readonly (string | RegExp)[];
    paths?: readonly StripPath[];
  };
  keep?: {
    paths?: readonly StripPath[];
  };
  maxDepth?: number;
  maxNodes?: number;
};

type ShapeResolver<TRow> = (row: TRow) => unknown;

type ShapeField<TRow> =
  | string
  | readonly StripPathSegment[]
  | ShapeResolver<TRow>
  | ShapeArray<TRow>
  | ShapeObject<TRow>
  | null;

type ShapeArray<TRow> = {
  from: StripPath;
  each: ShapeObject<unknown> | ShapeResolver<unknown>;
};

type ShapeObject<TRow> = {
  [outputKey: string]: ShapeField<TRow>;
};