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

@innerskylabs/safety

v0.0.1

Published

Check text and images against named safety policies.

Readme

@innerskylabs/safety

Check text and images against named safety policies.

npm install @innerskylabs/safety

Node 18+. Ships ESM and CJS with full types. Every call is plain fetch, and there are no other dependencies.

The surface is deliberately small: save/get/list/remove on policies, and checkText/checkImage on the client. That is all.

60-second tour

Write a policy once, offline — a setup script, an admin panel, CI. Anywhere with an API key, run once, not per request:

import { Policies } from '@innerskylabs/safety';

const creds = { apiKey: process.env.SAFETY_API_KEY! };

// A policy SET is one string. Write it however you like — a numbered list, a
// paragraph — and the service works out the distinct rules in it.
await new Policies(creds).save('uploads', `
  1) No threats of violence, weapons, or firearms — shown, described, or implied.
  2) No sharing of personal phone numbers or home addresses.
`);

// One rule needs no numbering.
await new Policies(creds).save('comments', 'Block harassment and threats.');

Sets never interact. A check names one set and is judged against that set alone, so uploads and comments can disagree with each other freely — only rules within one set have to agree.

The key is the whole credential. A key belongs to one project, and the project is what scopes everything it can reach: its policy sets and its rate limit. So uploads in your staging project and uploads in production are two different sets, and you point a service at one or the other by changing its key — there is no second id to keep in step.

Then check either input type against it, online, as many times as you want:

import { SafetyClient } from '@innerskylabs/safety';

const client = new SafetyClient(creds);

const text = await client.checkText('I am going to find him and beat him', 'uploads');
text.verdict;   // 'block' | 'pass'

const image = await client.checkImage('./loading-dock.jpg', 'uploads');
image.verdict;  // 'block' | 'pass' | 'unknown'

The shape of it

checkText(text, policySet) and checkImage(image, policySet) take their arguments in the same order and return the same verdict field. Pass { showReason: true } as a third argument to get a reason too; on text that changes how some checks are judged, so it's off by default. policies.save() upserts, so there is no create-or-update branch to write. It waits while the service splits, checks and compiles the set (tens of seconds), and throws if that fails or takes over five minutes. It resolves with { savedForTextUse, savedForImageUse }: whether text and image checks can use the set. The set is prepared for each kind of check separately, so one can fail while the other works; a check of a kind that failed throws, saying why. Credentials go to a constructor once rather than to every call. By default the client talks to https://api.safety.innerskylabs.ai; override with baseUrl on Credentials, or set SAFETY_BASE_URL for every instance in one process.

Every check names a saved policy. There is no free-text question mode, and that is measured rather than tidy: the ad-hoc route picks a polarity per input and answers YES measurably more often than a compiled policy does on the same content. One route means every verdict is produced the same way and traces back to prose somebody reviewed.

Reading a result

import { isSafe } from '@innerskylabs/safety';

if (!isSafe(result)) await reject();

isSafe returns true only for pass. unknown reads as unsafe, and so does any verdict a future deployment adds.

A result carries two fields and no more — verdict and reason. The route it took, the model calls it made and the facts it asked never change what a caller does, so they aren't on it.

Saving a set

Whatever you write is saved as you wrote it. Nothing lints it and nothing rewrites it. A save can be refused, with an Error saying why — for the rules contradicting each other (one allowing what another blocks), nothing in it being a rule about content, too many rules or conditions, the text or name being too long, another save of it still in progress, or the save taking over five minutes. The one worth knowing about in advance is two rules in the set that contradict each other — one allowing what another blocks:

try {
  await policies.save('house-rules',
    '1) Block content about firearms. 2) Allow content about firearms.');
} catch (err) {
  console.error(err.message);            // names the two rules
}

It catches more than exact opposites: "block religion" plus "allow questions about Jesus" is a contradiction too, because every Jesus question is religion.

Your string is split into its distinct rules by a model before anything compares them, so a paragraph works as well as a numbered list — "No weapons. This includes replicas." stays one rule, while "No weapons. No doxxing." becomes two. Numbering is still the clearest way to say what you mean, and it is honoured exactly when you use it.

That refusal is why no check ever returns a contradiction verdict — a set that argues with itself cannot get into the system in the first place.

Saving does real work, so it is not a cheap call. Run it from a deploy script or an admin tool, not per request.

Errors

Every failure is a plain Error whose message says what went wrong — the service's own description when it answered, or why it could not be reached.

catch (err) {
  logger.error(err.message);
}

A public upload endpoint

A browser must never hold an API key. The page calls your endpoint, and your server calls this SDK:

app.post('/api/check-upload', upload.single('file'), async (req, res) => {
  const result = await client.checkImage(req.file.buffer, 'upload-content-standards');
  res.json({ allowed: isSafe(result) });
});

Notes on what is not here

No profanity check. The design doc sketched one as a local word list. It was a demonstration of a shape rather than a usable filter, and it is not part of the SDK's job: the SDK checks content against policies you wrote.

No Policies.lint() and no preview(). Whatever you write is saved as you wrote it. How the service prepares a set to judge with is an implementation detail — there is nothing a caller can act on there, and putting it on the wire would be a promise to keep it stable.

No client.report(). It was surface nothing called.

No isUserDefined on a policy set. Every set is one you wrote, so the flag was true on every row it could ever appear on. A field with one possible value tells a caller nothing and is one more thing to keep in step.